diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..d530245 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,4 @@ +.git +target +**/target +**/config.toml diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9a8c8a1 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug report +about: Report a reproducible bug +title: "bug: " +labels: bug +assignees: "" +--- + +## Description + + + +## Steps to reproduce + + + +## Expected behaviour + + + +## Actual behaviour + + + +## Environment + +- **OS**: +- **Rust version**: +- **Commit / tag**: +- **Affected crate(s)**: + +## Logs + + + +```plaintext +paste here +``` + +## Additional context + + diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..763d64a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability + url: https://github.com/BlockstreamResearch/simplicity-unchained/security/advisories/new + about: Please report security issues privately via GitHub's security advisory feature rather than opening a public issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..5ab9599 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,27 @@ +--- +name: Feature request +about: Propose a new feature or improvement +title: "feat: " +labels: enhancement +assignees: "" +--- + +## Problem / motivation + + + +## Proposed solution + + + +## Affected crate(s) + + + +## Alternatives considered + + + +## Additional context + + diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..761b07f --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ +# Summary + + + +## Type of change + +- [ ] Bug fix +- [ ] New feature +- [ ] Refactor +- [ ] Documentation +- [ ] Dependency update +- [ ] Other (describe below) + +## Affected crates + +- [ ] `core` +- [ ] `service` +- [ ] `cli` +- [ ] CI / tooling + +## Checklist + +- [ ] `cargo fmt --all` passes +- [ ] `cargo clippy --workspace --all-targets -- -D warnings` passes +- [ ] `cargo test --workspace --all-targets` passes + +## Breaking changes + + + +## Testing + + + +## Related issues + + diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..a268bea --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,43 @@ +name: CI + +on: + push: + branches: [master, development] + pull_request: + branches: [master, development] + +jobs: + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92.0 + - uses: Swatinem/rust-cache@v2 + - run: cargo build --workspace --all-targets + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92.0 + - uses: Swatinem/rust-cache@v2 + - run: cargo build --workspace --all-targets + - run: cargo test --workspace + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.92.0 + components: clippy + - uses: Swatinem/rust-cache@v2 + - run: cargo clippy --workspace --all-targets -- -D warnings diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7ec37bf --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,51 @@ +name: Publish release image + +on: + release: + types: [published] + +jobs: + publish: + name: Publish service image to GHCR + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Check out release tag + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: refs/tags/${{ github.event.release.tag_name }} + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Log in to GHCR + uses: docker/login-action@8aa5831fb8d48d8f76594c7ddde61ae58019f550 # v4.8.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image tags and labels + id: meta + uses: docker/metadata-action@dc802804100637a589fabce1cb79ff13a1411302 # v6.2.0 + with: + images: ghcr.io/${{ github.repository }} + flavor: latest=false + tags: | + type=raw,value=${{ github.event.release.tag_name }} + type=raw,value=latest,enable=${{ !github.event.release.prerelease }} + + - name: Build and publish image + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/.gitignore b/.gitignore index 3413ebb..042f13f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ /target /core/target *.dev.* +/config.toml +/service/config.toml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4ea6a0b --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,89 @@ +# Contributing to Simplicity Unchained + +Contributions of all kinds are welcome — bug reports, documentation improvements, and tests. + +## Project Structure + +The workspace is organised into focused crates. Keep changes scoped to the appropriate crate and discuss cross-crate changes in an issue first. + +- [`policy`](policy/README.md): Portable `bitcoin-policy-v1` registry and verified context. +- [`core`](core/README.md): Policy execution adapter and shared Bitcoin/Elements primitives. +- [`service`](service/README.md): Axum HTTP service exposing separate Bitcoin/Liquid policy APIs and the version endpoint. +- [`cli`](cli/README.md): Primary `policy` workflow and key generation. + +## Issues + +If this is just a bug report or a feature request, please open an issue first so it can be discussed. PRs for bug fixes are also welcome and can be discussed directly in the PR. However, for larger changes, it is better to open an issue first to discuss the proposed change before implementing it. + +### Pull Requests + +1. Fork the repository and create a branch from `development`. +2. Make your changes, including tests where applicable. +3. Ensure CI checks pass. +4. Open a PR against `development` describing what was changed and why. +5. Keep PRs focused, one logical change per PR. Split large changes into sequential PRs if needed. + +## Coding Conventions + +- Run `cargo fmt` before every commit. +- The project targets zero `clippy` warnings with `-D warnings`. Fix all warnings introduced by your changes. +- Use `thiserror` for library errors. Avoid `unwrap()` outside of tests and examples. +- Keep the public API surface minimal. Only `pub` what downstream crates genuinely need, and add `///` doc comments to all new public items. +- Adding a new dependency requires justification. Prefer `[workspace.dependencies]` entries and avoid floating version requirements. +- Add unit tests under a `#[cfg(test)]` module in the same file. + +## Running the Service Locally + +First follow the service's [key-provisioning instructions](service/README.md#starting-the-service). +The repository no longer supplies an active configuration or a usable signing key. + +```bash +cd service +cargo run --quiet -- start +``` + +The service reads `config.toml` from the current directory by default. You can point it at a custom file with `--config`: + +```bash +cargo run --quiet -- start --config path/to/your/config.toml +``` + +To exercise the policy CLI against a matching Testnet4 service, follow the +[demo's artifact requirements](cli/README.md#demo): + +```bash +./cli/scripts/demo_btc.sh --help +# With reviewed policy artifacts configured: +./cli/scripts/demo_btc.sh review +``` + +## Docker + +The multi-stage `Dockerfile` builds the `service` binary. To build and run: + +```bash +docker build -t simplicity-unchained-service . +docker run --rm -p 30431:30431 \ + --mount type=bind,src=/absolute/path/to/service-config.toml,dst=/app/config.toml,readonly \ + simplicity-unchained-service +``` + +Provision the host configuration outside the Docker build context with a unique +signing key; see the service [Docker instructions](service/README.md#docker). +The container runs as a non-root user (`uid=1000`). Ensure the private mounted +configuration is readable by that user. An unconfigured container exits unsuccessfully. + +## Reporting Bugs + +Open a GitHub issue including: + +1. What happened vs. what you expected. +2. A minimal command sequence or code snippet to reproduce the issue. +3. Your OS, Rust version (`rustc --version`), and commit hash. +4. Relevant error output with sensitive data redacted. + +For security-sensitive issues, please follow responsible disclosure and contact the maintainers privately before opening a public issue. + +## Licence + +By contributing to this project you agree that your contributions are released under the [CC0 1.0 Universal](LICENCE) licence, the same licence that covers the project. diff --git a/Cargo.lock b/Cargo.lock index 6db90da..0b87c40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,33 +2,6 @@ # It is not intended for manual editing. version = 4 -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "ansi_term" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee49baf6cb617b853aa8d93bf420db2383fab46d314482ca2803b40d5fde979b" -dependencies = [ - "winapi", -] - [[package]] name = "anstream" version = "0.6.21" @@ -97,23 +70,6 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - [[package]] name = "axum" version = "0.8.8" @@ -124,10 +80,10 @@ dependencies = [ "bytes", "form_urlencoded", "futures-util", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", - "hyper 1.8.1", + "hyper", "hyper-util", "itoa", "matchit", @@ -139,7 +95,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower", "tower-layer", @@ -155,12 +111,12 @@ checksum = "59446ce19cd142f8833f856eb31f3eb097812d1479ab224f54d72428ca21ea22" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "http-body-util", "mime", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -168,12 +124,11 @@ dependencies = [ [[package]] name = "base58ck" -version = "0.1.0" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +checksum = "365c0acd5b2e8dd0111a46c4faea83fb3cfb6e39a49a7c73a06e090db7b2eff0" dependencies = [ - "bitcoin-internals", - "bitcoin_hashes", + "bitcoin_hashes 0.14.101", ] [[package]] @@ -188,65 +143,62 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" -[[package]] -name = "base64-compat" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a8d4d2746f89841e49230dd26917df1876050f95abafafbe34f47cb534b88d7" -dependencies = [ - "byteorder", -] - [[package]] name = "bech32" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" -[[package]] -name = "bip39" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90dbd31c98227229239363921e60fcf5e558e43ec69094d46fc4996f08d1d5bc" -dependencies = [ - "bitcoin_hashes", - "serde", - "unicode-normalization", -] - [[package]] name = "bitcoin" -version = "0.32.8" +version = "0.32.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" +checksum = "bb0ce8bd5baaa0d303a19915a6d93afed161f528654e42da2a7a97d05c59499a" dependencies = [ "base58ck", "base64 0.21.7", "bech32", - "bitcoin-internals", + "bitcoin-consensus-encoding", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes", - "hex-conservative", + "bitcoin_hashes 0.14.101", + "hex-conservative 0.2.2", "hex_lit", "secp256k1", "serde", ] [[package]] -name = "bitcoin-internals" -version = "0.3.0" +name = "bitcoin-consensus-encoding" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +checksum = "9daa31138eb443d5751b207f3f64154e2bb09cd59960562ccc7a7112be38147f" dependencies = [ + "bitcoin-internals 0.7.0", + "hex-conservative 1.3.0", "serde", ] +[[package]] +name = "bitcoin-internals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d573f4cf32996a8dce612e4348cece65a241f1882ed594047c9ba348e8869fa5" + +[[package]] +name = "bitcoin-internals" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8bea3a9f0cfece4564e37cb49a38cc245ca5184719e50d7d0dda3268722c4e2" + [[package]] name = "bitcoin-io" -version = "0.1.4" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" +checksum = "bb5de036369d1ac59d3c1819ebc4d850f89466f5401c571a285b6ed564a4cb78" +dependencies = [ + "bitcoin-consensus-encoding", +] [[package]] name = "bitcoin-private" @@ -256,22 +208,34 @@ checksum = "73290177011694f38ec25e165d0387ab7ea749a4b81cd4c80dae5988229f7a57" [[package]] name = "bitcoin-units" -version = "0.1.2" +version = "0.1.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" +checksum = "9cb95693f371d089a4b5b6fc41c6f3ea6e01ee8c15388335dfac8ea685173b51" dependencies = [ - "bitcoin-internals", + "bitcoin-consensus-encoding", "serde", ] [[package]] name = "bitcoin_hashes" -version = "0.14.1" +version = "0.14.101" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +checksum = "bca4c7abb40c8817d77403c880988cfd484f23ab2365726afb2f798363e2c4a2" dependencies = [ "bitcoin-io", - "hex-conservative", + "hex-conservative 0.2.2", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5304e53726dbe5f93141535e102ed97b5bf4714fbecefdda8f9fb98d7fdaff0e" +dependencies = [ + "bitcoin-consensus-encoding", + "bitcoin-internals 0.6.0", + "hex-conservative 1.3.0", "serde", ] @@ -301,9 +265,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.11.0" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" @@ -321,35 +285,6 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" -[[package]] -name = "chrono" -version = "0.4.42" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "serde", - "wasm-bindgen", - "windows-link", -] - -[[package]] -name = "clap" -version = "2.33.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" -dependencies = [ - "ansi_term", - "atty", - "bitflags 1.3.2", - "strsim 0.8.0", - "textwrap", - "unicode-width", - "vec_map", -] - [[package]] name = "clap" version = "4.5.53" @@ -369,7 +304,7 @@ dependencies = [ "anstream", "anstyle", "clap_lex", - "strsim 0.11.1", + "strsim", ] [[package]] @@ -395,13 +330,14 @@ name = "cli" version = "0.1.0" dependencies = [ "anyhow", - "clap 4.5.53", + "clap", "core", - "hal-simplicity", - "hex 0.4.3", + "hex", "reqwest", "serde", "serde_json", + "simplicity-lang", + "simplicity-unchained-policy", ] [[package]] @@ -414,9 +350,13 @@ checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" name = "core" version = "0.1.0" dependencies = [ - "hal-simplicity", - "hex 0.4.3", + "dlopen2", + "hex", "hex-literal", + "jet_plugin_abi", + "script_jets", + "simplicity-lang", + "simplicity-unchained-policy", "thiserror", ] @@ -437,45 +377,33 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] -name = "darling" -version = "0.20.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.11" +name = "displaydoc" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ - "fnv", - "ident_case", "proc-macro2", "quote", - "strsim 0.11.1", "syn", ] [[package]] -name = "darling_macro" -version = "0.20.11" +name = "dlopen2" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" dependencies = [ - "darling_core", - "quote", - "syn", + "dlopen2_derive", + "libc", + "once_cell", + "winapi", ] [[package]] -name = "displaydoc" -version = "0.2.5" +name = "dlopen2_derive" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" dependencies = [ "proc-macro2", "quote", @@ -484,12 +412,16 @@ dependencies = [ [[package]] name = "elements" -version = "0.25.2" +version = "0.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b2569d3495bfdfce36c504fd4d78752ff4a7699f8a33e6f3ee523bddf9f6ad" +checksum = "34e00f7ddcd826d2f4c9bd72d79a6b6991a0ac4286e7e2b2ff8eecd130549073" dependencies = [ "bech32", "bitcoin", + "bitcoin-consensus-encoding", + "bitcoin-internals 0.6.0", + "bitcoin_hashes 1.2.0", + "hex-conservative 1.3.0", "secp256k1-zkp", "serde", "serde_json", @@ -526,28 +458,12 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" -[[package]] -name = "fern" -version = "0.5.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69ab0d5aca163e388c3a49d284fed6c3d0810700e77c5ae2756a50ec1a4daaa" -dependencies = [ - "chrono", - "log", -] - [[package]] name = "find-msvc-tools" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a3076410a55c90011c298b04d0cfa770b00fa04e1e3c97d3f6c9de105a03844" -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - [[package]] name = "foreign-types" version = "0.3.2" @@ -579,6 +495,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", + "futures-sink", ] [[package]] @@ -613,6 +530,7 @@ checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-core", "futures-io", + "futures-sink", "futures-task", "memchr", "pin-project-lite", @@ -651,76 +569,6 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8449d342b1c67f49169e92e71deb7b9b27f30062301a16dbc27a4cc8d2351b7" -[[package]] -name = "h2" -version = "0.3.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 0.2.12", - "indexmap 2.12.1", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hal" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f57a34f3cc18f2967a98c61abea03b1b9521075e880b18f45b63561a88c3c174" -dependencies = [ - "base64-compat", - "bip39", - "bitcoin", - "byteorder", - "chrono", - "clap 2.33.3", - "fern", - "hex 0.3.2", - "jobserver", - "lazy_static", - "lightning-invoice", - "log", - "miniscript", - "secp256k1", - "serde", - "serde_json", - "serde_yaml", - "shell-escape", -] - -[[package]] -name = "hal-simplicity" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52a540615bef9124e283bbd087e007af922c9f6cfac210412bda4d1c34b2a74d" -dependencies = [ - "clap 2.33.3", - "elements", - "fern", - "hal", - "hex 0.3.2", - "log", - "serde", - "serde_json", - "serde_yaml", - "simplicity-lang", - "thiserror", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - [[package]] name = "hashbrown" version = "0.16.1" @@ -733,21 +581,6 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "hex" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "805026a5d0141ffc30abb3be3173848ad46a1b1664fe632428479619a3644d77" - [[package]] name = "hex" version = "0.4.3" @@ -763,6 +596,15 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "hex-conservative" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271e0d19bcb473b6675739a2b536076b24a082316cb5199ad918edce10c599e8" +dependencies = [ + "arrayvec", +] + [[package]] name = "hex-literal" version = "1.1.0" @@ -775,17 +617,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.4.0" @@ -796,17 +627,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.1" @@ -814,7 +634,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" dependencies = [ "bytes", - "http 1.4.0", + "http", ] [[package]] @@ -825,8 +645,8 @@ checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" dependencies = [ "bytes", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "pin-project-lite", ] @@ -842,30 +662,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2 0.5.10", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.8.1" @@ -876,8 +672,8 @@ dependencies = [ "bytes", "futures-channel", "futures-core", - "http 1.4.0", - "http-body 1.0.1", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -885,19 +681,23 @@ dependencies = [ "pin-utils", "smallvec", "tokio", + "want", ] [[package]] name = "hyper-tls" -version = "0.5.0" +version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" dependencies = [ "bytes", - "hyper 0.14.32", + "http-body-util", + "hyper", + "hyper-util", "native-tls", "tokio", "tokio-native-tls", + "tower-service", ] [[package]] @@ -906,38 +706,24 @@ version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "727805d60e7938b76b826a6ef209eb70eaa1812794f9424d4a4e2d740662df5f" dependencies = [ + "base64 0.22.1", "bytes", + "futures-channel", "futures-core", - "http 1.4.0", - "http-body 1.0.1", - "hyper 1.8.1", + "futures-util", + "http", + "http-body", + "hyper", + "ipnet", + "libc", + "percent-encoding", "pin-project-lite", + "socket2", + "system-configuration", "tokio", "tower-service", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.64" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", + "tracing", + "windows-registry", ] [[package]] @@ -1021,12 +807,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.1.0" @@ -1048,16 +828,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", -] - [[package]] name = "indexmap" version = "2.12.1" @@ -1065,7 +835,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown", ] [[package]] @@ -1087,13 +857,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ee5b5339afb4c41626dde77b7a611bd4f2c202b897852b4bcf5d03eddc61010" [[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +name = "jet_plugin_abi" +version = "0.1.0" dependencies = [ - "getrandom 0.3.4", - "libc", + "simplicity-sys", ] [[package]] @@ -1118,32 +885,6 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" -[[package]] -name = "lightning-invoice" -version = "0.33.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11209f386879b97198b2bfc9e9c1e5d42870825c6bd4376f17f95357244d6600" -dependencies = [ - "bech32", - "bitcoin", - "lightning-types", -] - -[[package]] -name = "lightning-types" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cd84d4e71472035903e43caded8ecc123066ce466329ccd5ae537a8d5488c7" -dependencies = [ - "bitcoin", -] - -[[package]] -name = "linked-hash-map" -version = "0.5.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" - [[package]] name = "linux-raw-sys" version = "0.11.0" @@ -1227,15 +968,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - [[package]] name = "once_cell" version = "1.21.3" @@ -1334,28 +1066,6 @@ dependencies = [ "zerocopy", ] -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "proc-macro2" version = "1.0.103" @@ -1410,73 +1120,44 @@ dependencies = [ "getrandom 0.2.16", ] -[[package]] -name = "regex" -version = "1.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843bc0191f75f3e22651ae5f1e72939ab2f72a4bc30fa80a066bd66edefc24d4" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" - [[package]] name = "reqwest" -version = "0.11.27" +version = "0.12.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147" dependencies = [ - "base64 0.21.7", + "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.32", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-tls", - "ipnet", + "hyper-util", "js-sys", "log", "mime", "native-tls", - "once_cell", "percent-encoding", "pin-project-lite", - "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", - "sync_wrapper 0.1.2", - "system-configuration", + "sync_wrapper", "tokio", "tokio-native-tls", + "tower", + "tower-http", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "winreg", ] [[package]] @@ -1493,12 +1174,12 @@ dependencies = [ ] [[package]] -name = "rustls-pemfile" -version = "1.0.4" +name = "rustls-pki-types" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96" dependencies = [ - "base64 0.21.7", + "zeroize", ] [[package]] @@ -1513,15 +1194,6 @@ version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62049b2877bf12821e8f9ad256ee38fdc31db7387ec2d3b3f403024de2034aea" -[[package]] -name = "santiago" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de36022292bc2086eb8f55bffa460fef3475e4459b478820711f4c421feb87ec" -dependencies = [ - "regex", -] - [[package]] name = "schannel" version = "0.1.28" @@ -1531,13 +1203,20 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "script_jets" +version = "0.1.0" +dependencies = [ + "jet_plugin_abi", +] + [[package]] name = "secp256k1" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 0.14.101", "rand", "secp256k1-sys", "serde", @@ -1673,37 +1352,28 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_yaml" -version = "0.8.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" -dependencies = [ - "indexmap 1.9.3", - "ryu", - "serde", - "yaml-rust", -] - [[package]] name = "service" version = "0.1.0" dependencies = [ "axum", "base64 0.22.1", - "clap 4.5.53", + "clap", "core", - "hal-simplicity", - "hex 0.4.3", + "hex", + "http-body", "log", "serde", "serde_json", + "simplicity-lang", + "simplicity-unchained-policy", + "tempfile", "thiserror", "tokio", "toml", + "tower", "tracing", "tracing-subscriber", - "validator", ] [[package]] @@ -1715,12 +1385,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shell-escape" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" - [[package]] name = "shlex" version = "1.3.0" @@ -1729,33 +1393,43 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simplicity-lang" -version = "0.7.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e57bd4d84853974a212eab24ed89da54f49fbccf5e33e93bcd29f0a6591cd5" +checksum = "cff7b10251528d46d9cf60238267911984fedbb0948cccea98dd74fc6bf9f729" dependencies = [ "bitcoin", - "bitcoin_hashes", + "bitcoin_hashes 1.2.0", "byteorder", "elements", "getrandom 0.2.16", "ghost-cell", - "hex-conservative", + "hex-conservative 1.3.0", "miniscript", - "santiago", - "serde", "simplicity-sys", ] [[package]] name = "simplicity-sys" -version = "0.6.1" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bcb4e5bfc15080d67e0ce2c17d1c31bfb7521d65c86ea26ed0de72d5119d119" +checksum = "955935733b2735665b4735d9b1361eade6be4c7aea6bca2640345e4c81756f58" dependencies = [ - "bitcoin_hashes", + "bitcoin_hashes 1.2.0", "cc", ] +[[package]] +name = "simplicity-unchained-policy" +version = "0.1.0" +dependencies = [ + "elements", + "hex", + "serde", + "serde_json", + "simplicity-lang", + "thiserror", +] + [[package]] name = "slab" version = "0.4.11" @@ -1768,16 +1442,6 @@ version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -[[package]] -name = "socket2" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" -dependencies = [ - "libc", - "windows-sys 0.52.0", -] - [[package]] name = "socket2" version = "0.6.1" @@ -1794,12 +1458,6 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.11.1" @@ -1817,17 +1475,14 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -1874,15 +1529,6 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - [[package]] name = "thiserror" version = "2.0.17" @@ -1922,21 +1568,6 @@ dependencies = [ "zerovec", ] -[[package]] -name = "tinyvec" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa5fdc3bce6191a1dbc8c02d5c8bffcf557bafa17c124c5264a458f1b0613fa" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - [[package]] name = "tokio" version = "1.48.0" @@ -1947,7 +1578,7 @@ dependencies = [ "libc", "mio", "pin-project-lite", - "socket2 0.6.1", + "socket2", "tokio-macros", "windows-sys 0.61.2", ] @@ -1973,26 +1604,13 @@ dependencies = [ "tokio", ] -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - [[package]] name = "toml" version = "0.9.10+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0825052159284a1a8b4d6c0c86cbc801f2da5afd2b225fa548c72f2e74002f48" dependencies = [ - "indexmap 2.12.1", + "indexmap", "serde_core", "serde_spanned", "toml_datetime", @@ -2034,13 +1652,31 @@ dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 1.0.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", "tracing", ] +[[package]] +name = "tower-http" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840" +dependencies = [ + "bitflags 2.10.0", + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", + "tower", + "tower-layer", + "tower-service", + "url", +] + [[package]] name = "tower-layer" version = "0.3.3" @@ -2123,21 +1759,6 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" -[[package]] -name = "unicode-normalization" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - [[package]] name = "url" version = "2.5.7" @@ -2162,36 +1783,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" -[[package]] -name = "validator" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43fb22e1a008ece370ce08a3e9e4447a910e92621bb49b85d6e48a45397e7cfa" -dependencies = [ - "idna", - "once_cell", - "regex", - "serde", - "serde_derive", - "serde_json", - "url", - "validator_derive", -] - -[[package]] -name = "validator_derive" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7df16e474ef958526d1205f6dda359fdfab79d9aa6d54bafcb92dcd07673dca" -dependencies = [ - "darling", - "once_cell", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn", -] - [[package]] name = "valuable" version = "0.1.1" @@ -2204,12 +1795,6 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - [[package]] name = "want" version = "0.3.1" @@ -2325,46 +1910,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" +name = "windows-link" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" [[package]] -name = "windows-interface" -version = "0.59.3" +name = "windows-registry" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" dependencies = [ - "proc-macro2", - "quote", - "syn", + "windows-link", + "windows-result", + "windows-strings", ] -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-result" version = "0.4.1" @@ -2383,31 +1944,13 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-sys" -version = "0.48.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" -dependencies = [ - "windows-targets 0.48.5", -] - -[[package]] -name = "windows-sys" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - [[package]] name = "windows-sys" version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" dependencies = [ - "windows-targets 0.53.5", + "windows-targets", ] [[package]] @@ -2419,37 +1962,6 @@ dependencies = [ "windows-link", ] -[[package]] -name = "windows-targets" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" -dependencies = [ - "windows_aarch64_gnullvm 0.48.5", - "windows_aarch64_msvc 0.48.5", - "windows_i686_gnu 0.48.5", - "windows_i686_msvc 0.48.5", - "windows_x86_64_gnu 0.48.5", - "windows_x86_64_gnullvm 0.48.5", - "windows_x86_64_msvc 0.48.5", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - [[package]] name = "windows-targets" version = "0.53.5" @@ -2457,148 +1969,58 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" dependencies = [ "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - [[package]] name = "windows_aarch64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[package]] -name = "windows_aarch64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - [[package]] name = "windows_aarch64_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[package]] -name = "windows_i686_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - [[package]] name = "windows_i686_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - [[package]] name = "windows_i686_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[package]] -name = "windows_i686_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - [[package]] name = "windows_i686_msvc" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[package]] -name = "windows_x86_64_gnu" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - [[package]] name = "windows_x86_64_gnu" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - [[package]] name = "windows_x86_64_gnullvm" version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[package]] -name = "windows_x86_64_msvc" -version = "0.48.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - [[package]] name = "windows_x86_64_msvc" version = "0.53.1" @@ -2611,16 +2033,6 @@ version = "0.7.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" -[[package]] -name = "winreg" -version = "0.50.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" -dependencies = [ - "cfg-if", - "windows-sys 0.48.0", -] - [[package]] name = "wit-bindgen" version = "0.46.0" @@ -2633,15 +2045,6 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "yoke" version = "0.8.1" @@ -2706,6 +2109,12 @@ dependencies = [ "synstructure", ] +[[package]] +name = "zeroize" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879" + [[package]] name = "zerotrie" version = "0.2.3" diff --git a/Cargo.toml b/Cargo.toml index 27b0553..c989421 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,11 +3,14 @@ resolver = "2" members = [ "cli", "core", - "service" + "jet_plugin_abi", + "policy", + "plugin_tests/script_jets", + "service", ] [workspace.dependencies] -hal-simplicity = "0.2.0" +simplicity-lang = { version = "0.9.0", features = ["base64"] } clap = { version = "4.5.53", features = ["derive"] } hex = "0.4" serde = { version = "1.0", features = ["derive"] } diff --git a/Dockerfile b/Dockerfile index e53d303..16c0279 100644 --- a/Dockerfile +++ b/Dockerfile @@ -13,15 +13,19 @@ RUN apt-get update && apt-get install -y \ WORKDIR /build # Copy workspace manifests -COPY Cargo.toml rust-toolchain.toml ./ +COPY Cargo.toml Cargo.lock rust-toolchain.toml ./ # Copy all workspace members (required by Cargo workspace) COPY cli ./cli COPY core ./core -COPY service ./service +COPY policy ./policy +COPY service/Cargo.toml ./service/Cargo.toml +COPY service/src ./service/src +COPY jet_plugin_abi ./jet_plugin_abi +COPY plugin_tests ./plugin_tests # Build the service in release mode -RUN cargo build --release --package service +RUN cargo build --locked --release --package service # Stage 2: Runtime FROM debian:bookworm-slim @@ -41,20 +45,17 @@ WORKDIR /app # Copy the compiled binary from builder COPY --from=builder /build/target/release/service /app/service -# Copy the configuration file -COPY service/config.toml /app/config.toml - # Change ownership to the service user RUN chown -R service:service /app # Switch to non-root user USER service -# Expose the service port (default: 8080) -EXPOSE 8080 +# Expose the example configuration's service port +EXPOSE 30431 # Set the entrypoint ENTRYPOINT ["/app/service"] -# Default command arguments +# Requires an operator-provided read-only configuration mount CMD ["start", "--config", "/app/config.toml"] diff --git a/README.md b/README.md index ddcae49..e81b232 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,129 @@ # Simplicity Unchained (Prototype Phase) -Simplicity Unchained brings [Simplicity](https://simplicity-lang.org/) to Bitcoin networks. It acts as an oracle that executes Simplicity programs and, on success, produces side effects such as co-signing Bitcoin transactions, triggering events, or performing any other action a server is capable of. +Simplicity Unchained brings [Simplicity](https://simplicity-lang.org/) to Bitcoin networks. It acts as an off-chain oracle that executes a committed Simplicity policy and, on success, co-signs a transaction. -The primary target is **Bitcoin** (including Bitcoin testnets), with additional support for Elements-based networks such as Liquid. +The primary target is **Bitcoin**, using `bitcoin-policy-v1`, with confidential multi-asset support for Liquid through `liquid-policy-v1`. The CLI and demo default to Bitcoin Testnet4 and Liquid Testnet. > ⚠️ **Warning: Project Under Development** > -> This project is still in active development and conceptualization. It is **not safe for use in production environments** at this time. Features and functionality may change, and there may be critical bugs or incomplete implementations. +> This project has not been independently audited and is **not safe for use in production environments**. + +## How It Works + +See [core/README.md](core/README.md) for the unchained model, Taproot tree structure and policy co-signing flow. + +## Run your own SU service + +The [published image](https://github.com/BlockstreamResearch/simplicity-unchained/pkgs/container/simplicity-unchained) +runs the policy co-signer, not a Bitcoin or Liquid full node. You need Docker +Engine (or Docker Desktop) and `curl`; no Rust build or repository checkout is +required. This release targets `linux/amd64`; ARM hosts need amd64 emulation. +Use testnet keys and coins only. + +### 1. Provision the signing key + +Run once in a POSIX shell. The configuration is stored in a Docker volume with a +fresh random key, readable only by the container's service user (UID 1000). +The command refuses to overwrite an existing configuration. +For an existing funded service, reuse its private configuration instead of +generating a replacement key. + +```sh +IMAGE=ghcr.io/blockstreamresearch/simplicity-unchained:v0.1.0-alpha.0 +docker pull --platform linux/amd64 "$IMAGE" +docker volume create su-config + +docker run --rm --platform linux/amd64 --user 0:0 \ + --entrypoint /bin/sh \ + --mount type=volume,src=su-config,dst=/config \ + "$IMAGE" -euc ' + set -C + umask 077 + if [ -e /config/config.toml ]; then + echo "Configuration exists; reuse it." >&2 + exit 1 + fi + key=$(od -vAn -N32 -tx1 /dev/urandom | tr -d " \n") + [ "${#key}" -eq 64 ] + printf "%s\n" \ + "[service]" \ + "port = 30431" \ + "private_key = \"$key\"" \ + "bitcoin_network = \"testnet4\"" \ + "liquid_network = \"liquidtestnet\"" \ + > /config/config.toml + chown 1000:1000 /config/config.toml + ' +``` + +### 2. Start the service + +In the same shell: + +```sh +docker run -d --name simplicity-unchained \ + --platform linux/amd64 \ + --restart unless-stopped \ + --publish 127.0.0.1:30431:30431 \ + --mount type=volume,src=su-config,dst=/app/config,readonly \ + "$IMAGE" start --config /app/config/config.toml +``` + +The service runs as UID 1000 and reads its configuration from a read-only mount. +The API is available only on the Docker host. For remote clients, put it behind +an HTTPS reverse proxy; the service itself does not provide TLS. + +### 3. Check the API + +```sh +curl -fsS http://127.0.0.1:30431/simplicity-unchained/version +curl -fsS http://127.0.0.1:30431/simplicity-unchained/policy/bitcoin/capabilities +curl -fsS http://127.0.0.1:30431/simplicity-unchained/policy/liquid/capabilities +``` + +The version response is `{"version":"0.1.0"}`. Capabilities report each profile, +network and `base_public_key_hex`; pin that public key in clients before funding. +Use `docker logs simplicity-unchained` to diagnose startup failures. + +Stop with `docker stop simplicity-unchained` and start again with +`docker start simplicity-unchained`. Keep a secure backup of the private +configuration in `su-config`; deleting the volume or changing its key changes +the signer identity and can make cooperative spending impossible. +See the [service documentation](service/README.md) for configuration and API schemas. ## Project Structure -- [`core`](core/README.md): Contains the fundamental logic for executing Simplicity programs and interacting with Bitcoin and Elements environments. (Note: In the future, parts of this logic may be migrated to [hal-simplicity](https://github.com/BlockstreamResearch/hal-simplicity)) +- [`core`](core/README.md): Executes Simplicity policies and provides cryptographic, Taproot and transaction-environment utilities. -- [`service`](service/README.md): Implements a web service providing endpoints to interact with Simplicity Unchained. Currently, it supports a 2-of-2 multisig co-signing flow on Bitcoin and Elements, triggered by the successful execution of a Simplicity program. +- [`service`](service/README.md): Web service for policy-bound key derivation, transaction preparation, evaluation and co-signing. -- [`cli`](cli/README.md): Command-line interface for communicating with the Simplicity Unchained service and accessing various ecosystem utilities. (Note: In the future, parts of this CLI will use [hal-simplicity](https://github.com/BlockstreamResearch/hal-simplicity)) +- [`cli`](cli/README.md): Command-line policy workflows and key generation. -Each component of the project has its own README file with more detailed information. +- [`policy`](policy/README.md): Portable Bitcoin and Liquid policy kernels and registries, shared by native clients and the browser/WASM implementation. -## Web Demo +Each component has its own README with more detailed information. -A live testnet demo is hosted at **[testnet.simplicity-unchained.blockstream.com](https://testnet.simplicity-unchained.blockstream.com/)**. It provides a browser interface for writing Simplicity programs, compiling them, generating a P2WSH address, and sending co-signed transactions on Bitcoin Testnet4 or Liquid Testnet — no CLI or local setup required. +## Web Demo -![Simplicity Unchained Web Demo](doc/demo_website.png) +The [web demo](https://github.com/BlockstreamResearch/simplicity-unchained-web-demo) provides a browser interface for writing SimplicityHL policies, compiling them, deriving funding addresses and co-signing transactions on Bitcoin Testnet4 or Liquid Testnet. It opens on a landing page, with the interactive workflow at `/demo`. ## Capabilities demoed so far ### Bitcoin -You can explore the Bitcoin capabilities by running the [Bitcoin demo script](cli/scripts/demo_bitcoin.sh). This demo guides you through: +The [Bitcoin demo script](cli/scripts/demo_btc.sh) uses `bitcoin-policy-v1` on Bitcoin Testnet4: -1. Tweaking a public key with a Simplicity program via the Simplicity Unchained service. -2. Creating a 2-of-2 multisig P2WSH address on Bitcoin Testnet4. -3. Funding the address and creating a PSBT spending transaction. -4. Using the Simplicity Unchained service to co-sign the PSBT after successful Simplicity program execution. -5. Broadcasting the fully signed transaction to Bitcoin Testnet4. +1. Check the service's profile, network and known signing key. +2. Derive a policy-bound P2TR address before funding. +3. Prepare and review a PSBT using full parent transactions. +4. Evaluate the policy with its witness and request the service signature. +5. Add the user's signature and finalize locally. -### Elements / Liquid +See the [CLI readme](cli/README.md#demo) for setup and the [policy examples](examples/policy/README.md) for recipient restrictions, debit and fee caps, and transaction approvals. -The [Elements demo script](cli/scripts/demo.sh) demonstrates the same 2-of-2 multisig co-signing flow on the Liquid Testnet. +### Elements / Liquid -To run either demo, follow the instructions in the [CLI readme](cli/README.md). +The [`liquid-policy` CLI](cli/README.md#liquid-policy-workflow) supports confidential PSETs and multiple assets. It verifies asset/value disclosure proofs and per-asset accounting before evaluation and signing. See the [Liquid profile](policy/LIQUID.md) for schemas and asset semantics. ## Licence diff --git a/SECURITY-REVIEW.md b/SECURITY-REVIEW.md new file mode 100644 index 0000000..e8242da --- /dev/null +++ b/SECURITY-REVIEW.md @@ -0,0 +1,110 @@ +# Security review + +Date: 2026-11-06 + +## Scope + +Reviewed the native SU working tree based on `7a8853e`, including modified and +untracked policy code. This report does not commit or snapshot those changes. + +The review covered service routes and admission limits, Bitcoin and Liquid +transaction evidence, policy commitments and approvals, execution bounds, CLI +signature verification and finalization, native plugin trust boundaries, and +locked Rust dependencies. + +The browser demo, deployment configuration, infrastructure and operator key +storage were not audited. This was not a formal verification or penetration +test. No transactions were funded or broadcast, and the running service and +signer configuration were not changed. + +## Findings + +No application-level policy bypass or fund-theft exploit was confirmed. RustSec +identified two affected dependencies. Confidence below refers to the affected +locked version, not to exploitability through SU's current request paths. +Severity reflects review triage priority. + +| # | Severity | File | Lines | Vulnerability | Confidence | +|---|----------|------|-------|---------------|------------| +| 1 | 🟡 MEDIUM | [Cargo.lock](Cargo.lock) | 267-270 | `bytes` 1.11.0: integer overflow in `BytesMut::reserve` can cause memory corruption | 10/10 | +| 2 | ⚪ LOW | [Cargo.lock](Cargo.lock) | 577-580 | `h2` 0.3.27: empty DATA frames can accumulate without limit | 10/10 | + +### 1. bytes: RUSTSEC-2026-0007 + +The dependency is used by both the service and CLI. In release builds, an +unchecked capacity addition can wrap and permit out-of-bounds access. + +The upstream reproduction requires a reservation near `usize::MAX` after +splitting a buffer. No SU request path reaching that condition was identified; +the public policy routes limit request bodies to 2 MiB. The advisory's unsafe +reproduction was not executed. + +Upgrade `bytes` to **1.11.1 or later** and re-run the relevant tests. + +Source: [RustSec advisory](https://rustsec.org/advisories/RUSTSEC-2026-0007.html). + +### 2. h2: RUSTSEC-2026-0258 + +The affected dependency chain is: + +```text +cli -> reqwest 0.11.27 -> hyper 0.14.32 -> h2 0.3.27 +``` + +A peer can supply empty HTTP/2 DATA frames that consume no flow-control bytes +but still occupy receive-queue entries. Undrained streams can exhaust memory. + +The signing service does not depend on this `h2` version and is built with +HTTP/1 support only. The CLI's current native-TLS features do not enable ALPN, +and it does not request HTTP/2 prior knowledge. No active HTTP/2 exposure was +identified in the reviewed build. + +The fix is **h2 0.4.16 or later**. Since the current Hyper dependency requires +the 0.3 series, update the HTTP client dependency chain rather than forcing an +incompatible `h2` version. + +Source: [RustSec advisory](https://rustsec.org/advisories/RUSTSEC-2026-0258.html). + +## Other observations + +- RustSec also reported unmaintained `pin-utils` and `rustls-pemfile`, and + unsoundness warnings for `anyhow`, `rand` and `tokio`. These were not counted as + demonstrated SU vulnerabilities. +- The [native plugin loader](core/src/plugin/loader.rs) documents a race between + optional file-hash verification and opening the library. Keep plugin files and + their directories protected from untrusted writes. Plugins are operator-trusted + native code and are not loaded through the policy endpoints. +- Parent transaction verification establishes content, not confirmation or + unspentness. Stateless caps, independent recovery paths and operator-held + signing keys are documented trust constraints, not guarantees established by + this review. + +## Verification + +RustSec advisories were fetched during the review. `cargo audit` scanned 248 +locked dependencies and exited with two vulnerabilities and five warnings. +Dependency and feature trees confirmed the package chains and HTTP/2 conditions. + +| Command | Result | +|---|---| +| `cargo audit` | Failed with the two dependency advisories above. | +| `cargo tree --locked --offline -i h2@0.3.27` | Passed; dependency is CLI-only. | +| `cargo tree --locked --offline -i bytes@1.11.0` | Passed; dependency is shared by service and CLI. | +| `cargo tree --locked --offline -e features -i reqwest` | Passed; native-TLS ALPN feature is absent. | +| `cargo tree --locked --offline -e features -i axum` | Passed; service HTTP/2 feature is absent. | +| `cargo test -p service -p simplicity-unchained-policy -p core --locked` | Core: 30 passed, 1 ignored. Service unit tests: 20 passed. Startup: 7 passed, 1 failed with a connection reset; run stopped before the kernel tests. | +| `cargo test -p simplicity-unchained-policy --locked` | Passed: 20 Bitcoin kernel tests and 11 Liquid tests. | +| `cargo test -p service --test startup explicit_read_only_configuration_starts_and_serves_requests --locked -- --exact --test-threads=1` | Passed: the initially failing startup test. | +| `cargo test -p service --test startup --locked -- --test-threads=1` | Passed: all 8 startup tests. | +| `cargo test -p cli --test policy --test liquid_policy --test public_surface --test policy_templates --locked` | Passed: 32 tests, 1 ignored. | + +After reruns, **121 distinct selected tests passed; two were ignored**. These +included parent/proof tampering, stale approvals, wrong keys and signatures, +profile/network mismatches, multi-input finalization, body limits, admission +timeouts and preallocation cost rejection. Passing tests do not establish the +absence of other vulnerabilities. + +## Remediation status + +No fixes were applied. Update the affected dependencies, review the resulting +HTTP client behavior, and repeat the audit and tests before deployment. diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 560d840..fe67a2f 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -11,8 +11,9 @@ path = "src/main.rs" anyhow = "1.0" clap = { workspace = true } core = { path = "../core" } -hal-simplicity = { workspace = true } +simplicity-unchained-policy = { path = "../policy" } +simplicity-lang = { workspace = true } hex = { workspace = true } -reqwest = { version = "0.11", features = ["blocking", "json"] } +reqwest = { version = "0.12.28", default-features = false, features = ["blocking", "json", "native-tls", "charset", "system-proxy"] } serde = { workspace = true } serde_json = { workspace = true } diff --git a/cli/README.md b/cli/README.md index aeed730..dbbf71b 100644 --- a/cli/README.md +++ b/cli/README.md @@ -1,47 +1,224 @@ # Simplicity Unchained CLI -Command-line interface for communicating with the Simplicity Unchained service and accessing various ecosystem utilities. +Command-line interface for communicating with the Simplicity Unchained service and preparing, signing and finalizing policy transactions. ## Commands -You can easily access command descriptions and information about available arguments by using the `--help` flag with any command. There is no need to duplicate this information here. - -```man -A CLI tool for Simplicity Unchained utilities +Run from the repository root with `cargo run --quiet -p cli -- `, or use the built `simplicity-unchained` binary. Use `--help` with any command or subcommand for its arguments. +```plaintext Usage: simplicity-unchained Commands: - address Address operations - keypair Keypair operations - tx Transaction operations - help Print this message or the help of the given subcommand(s) + liquid-policy Confidential multi-asset Liquid policy utilities + policy Bitcoin policy funding, evaluation and signing + keypair Keypair operations + help Print this message or the help of the given subcommand(s) +``` + +### keypair + +- `keypair generate` — Generate a new secp256k1 keypair. + +## Bitcoin policy workflow + +Compile programs for `bitcoin-policy-v1`. See the [policy examples](../examples/policy/README.md) for recipient allowlists, debit and fee caps, threshold approvals and source/locktime restrictions. + +| `policy` operation | Behavior | +|---|---| +| `capabilities` | Check the service's registry, fingerprint, network and genesis hash. | +| `tweak` | Derive and verify the service's policy-bound cooperative key before funding. | +| `build-csv-leaf` | Build a Bitcoin recovery script from `--user-pubkey` and `--timelock` (1–65535 blocks). Retain this script privately. | +| `address` | Derive the funding address locally from the program, network, base service key, user key and recovery hash. | +| `prepare` | Compare the service's context with locally verified full parent evidence, without approval signatures. | +| `evaluate` | Locally evaluate the witnessed policy and compare the service context and bounded cost; no transaction signature. | +| `approval-hash` | Locally compute J53/J54 from verified parents; no signing-service contact. | +| `sign` | Evaluate the policy, request the service signature and verify the returned signature and PSBT. | +| `user-sign` | Verify the context and service signature, then add the user's signature locally using `--secret-key-file`. | +| `finalize` | Verify the policy binding, parent evidence and both signatures for **every** input before producing transaction hex. No broadcast. | + +`--profile` defaults to `bitcoin-policy-v1`; `--profile-hash` can pin the compiled fingerprint. `--network` defaults to **`testnet4`** and must match the service. Accepted networks are `bitcoin`, `testnet` and `testnet4`. + +`--service` defaults to `http://127.0.0.1:30431`. Supply an HTTP(S) origin without an endpoint path, credentials, query or fragment. Use TLS for remote deployments. Remote Bitcoin calls use `/simplicity-unchained/policy/bitcoin`. -Options: - -h, --help Print help +### Before funding + +Choose the reviewed program, exact script data, independently known base signing +key and user/recovery keys first. `tweak` returns `public_key_hex` as an **x-only** +cooperative leaf key, not the Taproot internal key. + +```bash +SERVICE_ORIGIN=http://127.0.0.1:30431 +cargo run --quiet -p cli -- policy capabilities \ + --network testnet4 --service "$SERVICE_ORIGIN" +cargo run --quiet -p cli -- policy build-csv-leaf \ + --user-pubkey "$USER_PUBLIC_KEY_HEX" --timelock 144 +# Retain its script and use its raw leaf_hash as RECOVERY_LEAF_HASH_HEX. +POLICY_ARGS=( + --network testnet4 + --program "$POLICY_PROGRAM_BASE64" + --redeem-script-hex "$REDEEM_SCRIPT_HEX" +) +cargo run --quiet -p cli -- policy tweak "${POLICY_ARGS[@]}" \ + --base-public-key "$BASE_SERVICE_PUBLIC_KEY_HEX" --service "$SERVICE_ORIGIN" +cargo run --quiet -p cli -- policy address "${POLICY_ARGS[@]}" \ + --base-public-key "$BASE_SERVICE_PUBLIC_KEY_HEX" \ + --user-pubkey "$USER_PUBLIC_KEY_HEX" \ + --user-leaf-hash-hex "$RECOVERY_LEAF_HASH_HEX" ``` -## Demo +`--program` takes standard base64. `--redeem-script-hex` commits exact bytes +(empty by default); changing them changes the address. `address` works offline +with the known base key and compiled profile. Check service capabilities and a +key-pinned `tweak` before funding. -You can run the [demo script](scripts/demo.sh) to see example usages of the CLI commands and an example of Simplicity Unchained capabilities. +### Prepare, approve and co-sign -Firstly, ensure you have the Simplicity Unchained service running. +Construct an unsigned version-2 PSBT using your Bitcoin wallet or PSBT tooling. +All inputs must spend the same policy-bound P2TR output, with one to eight inputs +and outputs. Provide full `non_witness_utxo` parents for **every** input. + +Parent verification is offline unless a resolver is configured, and fails if +evidence is missing. Use `--offline` to forbid fetching, or set +`--esplora-url https://mempool.space/testnet4/api` (or your own network-matching +Esplora API). Missing parents are fetched from `/tx//raw`, deduplicated and +checked against the outpoints. Missing `witness_utxo` is populated from the +verified parent; conflicting values/scripts fail. Context is validated **before +contacting the signing service**. There are no automatic signing retries. ```bash -cd service -cargo run --quiet -- start +TX_ARGS=( + "${POLICY_ARGS[@]}" + --psbt "$PSBT_HEX" --input-index 0 + --user-pubkey "$USER_PUBLIC_KEY_HEX" + --user-leaf-hash-hex "$RECOVERY_LEAF_HASH_HEX" + --base-public-key "$BASE_SERVICE_PUBLIC_KEY_HEX" + --offline +) +cargo run --quiet -p cli -- policy approval-hash "${TX_ARGS[@]}" +cargo run --quiet -p cli -- policy prepare "${TX_ARGS[@]}" --service "$SERVICE_ORIGIN" +# Review the context, sign the decoded J54 or J53 bytes, and encode the witness. +cargo run --quiet -p cli -- policy evaluate "${TX_ARGS[@]}" \ + --witness "$APPROVAL_WITNESS_BASE64" --service "$SERVICE_ORIGIN" +cargo run --quiet -p cli -- policy sign "${TX_ARGS[@]}" \ + --witness "$APPROVAL_WITNESS_BASE64" --service "$SERVICE_ORIGIN" +``` + +`prepare` needs no witness. `evaluate`/`sign` require standard-base64 `--witness`; +use `--witness ''` only for a witness-free program. The returned `context` includes +amounts, fees, debit, output scripts, keys and verification provenance for review: + +- `policy_approval_hash` (J53) binds the selected input and actual signing digest. +- `policy_transaction_approval_hash` (J54) can deliberately authorize all + same-contract inputs in this transaction. +- Sign the **decoded 32-byte digest** directly with BIP-340, not ASCII hex or + another SHA256. Any approved transaction/context change requires fresh approvals. +- Static hashlocks and fixed-message signatures are replayable; caps apply per transaction. + +Repeat `sign` and `user-sign` for every input, using the latest `psbt_hex` each +time. `user-sign` takes all the same transaction arguments and a file containing +only the user's 64-character private-key hex (surrounding whitespace is allowed). +Keep this file private, never commit it, and never send it to the service. +The CLI checks the secret against the committed user key. + +```bash +# Rebuild TX_ARGS with the service-returned PSBT_HEX before each invocation. +cargo run --quiet -p cli -- policy user-sign "${TX_ARGS[@]}" \ + --secret-key-file "$USER_SECRET_KEY_FILE" +# Rebuild TX_ARGS with user-sign's psbt_hex; repeat for all remaining inputs. +cargo run --quiet -p cli -- policy finalize "${TX_ARGS[@]}" ``` -Then, in a separate terminal, navigate to the `cli` directory and execute the demo script: +Finalization verifies the committed canonical leaf/control block and both +SIGHASH_DEFAULT signatures; missing signatures or tampered metadata fail. +Broadcast the resulting `transaction_hex` using your wallet only after separately +checking chain status, fee rate and transaction acceptance. + +Bitcoin policy commands print JSON. Failures after argument parsing print +`{"error":"..."}`, diagnostic stderr and a nonzero exit. Clap usage errors print +normal stderr usage. `parent-txid-verified` proves parent **content**, not active +chain membership, confirmation, consensus validity or unspentness; +`chain_status_verified` is always false. + +## Liquid policy workflow + +Compile programs for `liquid-policy-v1`. The network defaults to `liquidtestnet`; +`liquid` is also supported. See the [Liquid profile](../policy/LIQUID.md) for +disclosure proofs, asset semantics and creation schemas. + +`liquid-policy registry` prints the manifest. Other operations, except +`generate-blinding-key`, accept `--request-file path.json`. A transaction request +has this shape: + +```json +{ + "profile": "liquid-policy-v1", + "network": "liquidtestnet", + "program": "", + "redeem_script_hex": "", + "base_public_key_hex": "", + "pset_hex": "", + "input_index": 0, + "user_pubkey": "", + "user_leaf_hash_hex": "", + "witness": "" +} +``` + +Optional `profile_hash` pins the compiled fingerprint. `address` can add +`blinding_public_key_hex` for a confidential address. `create` uses a separate +request shape with full parents and optional local blinding secrets; keep these +creation files private. Receiving secrets are not sent to the service. + +| `liquid-policy` operation | Behavior | +| --- | --- | +| `registry` | Print the local registry and fingerprint. | +| `generate-blinding-key` | Generate a receiving keypair locally; keep the secret private. | +| `address`, `tweak` | Derive the funding address or service key locally. | +| `capabilities` | Check the remote manifest, network, genesis hash and known base key. | +| `create` | Build and blind a multi-asset PSET with disclosure proofs. | +| `prepare`, `approval-hash` | Verify evidence and produce the local context or approval hashes. | +| `evaluate` | Evaluate the witnessed policy locally. | +| `sign` | Evaluate locally, request a service signature and verify the returned PSET and signature. | +| `user-sign` | Verify the service signature and add the user's signature using `--secret-key-file`. | +| `finalize` | Verify proofs and both signatures for every input, then produce transaction hex. | + +Only `capabilities` and `sign` contact the service. They accept `--service`, +defaulting to `http://127.0.0.1:30431`. ```bash -cd cli -./scripts/demo.sh +cargo run --quiet -p cli -- liquid-policy registry +cargo run --quiet -p cli -- liquid-policy capabilities --request-file request.json +cargo run --quiet -p cli -- liquid-policy prepare --request-file request.json +cargo run --quiet -p cli -- liquid-policy evaluate --request-file request.json +cargo run --quiet -p cli -- liquid-policy sign --request-file request.json ``` -> ⚠️ **Warning: Too many requests** -> -> The demo script interacts with the Liquid Testnet Faucet, which imposes a rate limit of 3 requests per minute per IP address. If you exceed this limit, the script may fail. If this happens, please wait at least one minute before running the script again. +Update `pset_hex` with each returned PSET and repeat `sign` and `user-sign` for +every input before `finalize`. Approval signatures use decoded J53/J54 digest +bytes, as in the Bitcoin workflow. + +## Demo + +Provision a unique private service key following the +[startup instructions](../service/README.md#starting-the-service), then run: + +```bash +cd service +cargo run --quiet -- start +# In another terminal, from the repository root: +./cli/scripts/demo_btc.sh --help +``` + +The Bitcoin script requires `cargo` and `jq`. It defaults to Testnet4 and has +three modes: `address`, `review` (default) and `sign`. Its `--help` lists the +required environment variables for the program, script data, base service key, +user key and recovery hash. `review` and `sign` also require a PSBT and witness; +`sign` requires a private user key file. + +The script handles one input. It does not fetch faucet funds or broadcast, and +`review` does not request a signature. Use the commands above for multi-input +spends or Liquid. ## Licence diff --git a/cli/assets/check_opcode.simf b/cli/assets/check_opcode.simf index d6017cf..6530bf3 100644 --- a/cli/assets/check_opcode.simf +++ b/cli/assets/check_opcode.simf @@ -1,4 +1,5 @@ fn main() { + // Commit to the intended script via /tweak before funding, and use the same script when signing. // Test that the opcode extraction works correctly // // You can read any opcode from a script and build an additional branching logic on it. diff --git a/cli/assets/sig_verify_with_pubkey_from_script.simf b/cli/assets/sig_verify_with_pubkey_from_script.simf index e348cbd..39397b8 100644 --- a/cli/assets/sig_verify_with_pubkey_from_script.simf +++ b/cli/assets/sig_verify_with_pubkey_from_script.simf @@ -2,6 +2,7 @@ // // You can utilize pubkeys embedded in scripts to verify signatures against them. // Or do other cryptographic operations that require pubkeys. +// Commit to the intended script via /tweak before funding, and use the same script when signing. fn main() { let signature: [u8; 64] = 0x3d5e45a7e5e98964e523b19f460534ad68c2ace2056d6891fc2851bc88eb4a65ce0f7e69fb47fa2105d073df7619a2eaedff9c2685a5862c8c88eb13aa174bc2; diff --git a/cli/scripts/demo.sh b/cli/scripts/demo.sh deleted file mode 100755 index 16405c2..0000000 --- a/cli/scripts/demo.sh +++ /dev/null @@ -1,127 +0,0 @@ -#!/bin/bash - -set -e - -# Wait for user confirmation. -pause() { read -p "Press Enter to continue..."; echo; echo; } - -# Wait for transaction to be visible in the mempool. -wait_for_transaction() { - local TXID=$1 - echo "Waiting for transaction to be visible in the mempool..." - for i in {1..30}; do - TX_DATA=$(curl -s "https://blockstream.info/liquidtestnet/api/tx/$TXID" 2>/dev/null) - if [ -n "$TX_DATA" ] && [ "$TX_DATA" != "Transaction not found" ]; then - - # For some unfathomable reason, the next call can sometimes return 404, like, how is that even possible? - sleep 10 - - echo "Transaction found in mempool!" - return 0 - fi - sleep 5 - done - - echo "Error: Transaction not found after waiting. Please try again." - exit 1 -} - -# Simple Simplicity program that always returns true -PROGRAM="zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA" -WITNESS="" - -NETWORK="liquid_testnet" -FAUCET_ADDRESS="tlq1qq2g07nju42l0nlx0erqa3wsel2l8prnq96rlnhml262mcj7pe8w6ndvvyg237japt83z24m8gu4v3yfhaqvrqxydadc9scsmw" - -echo "==== Step 0: Get Tweaked Public Key for Simplicity Program ====" -TWEAK_REQUEST=$(jq -n --arg program "$PROGRAM" '{program: $program}') - -TWEAK_RESPONSE=$(curl -s -X POST http://localhost:30431/simplicity-unchained/tweak \ - -H "Content-Type: application/json" \ - -d "$TWEAK_REQUEST") - -# Extract the tweaked public key from the response and set it as COSIGNER_PUBKEY -COSIGNER_PUBKEY=$(echo "$TWEAK_RESPONSE" | jq -r '.tweaked_public_key_hex') - -echo "Tweaked public key (Co-signer): $COSIGNER_PUBKEY" -echo - - -echo "==== Step 1: Generate User Keypair ====" -USER_KEYPAIR=$(cargo run --quiet keypair generate) -USER_SECKEY=$(echo "$USER_KEYPAIR" | jq -r '.secret_key') -USER_PUBKEY=$(echo "$USER_KEYPAIR" | jq -r '.public_key') - -echo "User secret key: $USER_SECKEY" -echo "User public key: $USER_PUBKEY" -echo - -echo "==== Step 2: Create 2-of-2 Multisig Address ====" -ADDRESS_DATA=$(cargo run --quiet -- address multisig --pubkey1 $COSIGNER_PUBKEY --pubkey2 $USER_PUBKEY --network $NETWORK) -ADDRESS=$(echo "$ADDRESS_DATA" | jq -r '.address') -REDEEM_SCRIPT=$(echo "$ADDRESS_DATA" | jq -r '.redeem_script') - -echo "Multisig P2WSH address: $ADDRESS" -echo "Redeem script: $REDEEM_SCRIPT" -echo - -echo "==== Step 3: Fund Address from Faucet ====" -echo "Running curl to connect to Liquid Testnet faucet..." -FAUCET_TRANSACTION=$(curl "https://liquidtestnet.com/faucet?address=$ADDRESS&action=lbtc" 2>/dev/null | sed -n "s/.*with transaction \([0-9a-f]*\)\..*$/\1/p") - -echo "Faucet transaction ID: $FAUCET_TRANSACTION" -echo - -wait_for_transaction "$FAUCET_TRANSACTION" -echo - -echo "==== Step 4: Create PSET ====" -PSET_CREATE_DATA=$(cargo run --quiet -- tx create -i "$FAUCET_TRANSACTION:0" -o "$ADDRESS:99000" -o "fee:1000" --network $NETWORK) -PSET_HEX=$(echo "$PSET_CREATE_DATA" | jq -r '.pset') - -echo "Created PSET (unsigned): $PSET_HEX" -echo - -echo "==== Step 5: First Signature (Co-signer) ====" -echo "Calling sign service at http://localhost:30431/simplicity-unchained/sign/pset..." - -SIGN_REQUEST=$(jq -n \ - --arg pset "$PSET_HEX" \ - --arg redeem "$REDEEM_SCRIPT" \ - --arg program "$PROGRAM" \ - --arg witness "$WITNESS" \ - '{pset_hex: $pset, redeem_script_hex: $redeem, input_index: 0, program: $program, witness: $witness}') - -PSET_SIGN1_DATA=$(curl -s -X POST http://localhost:30431/simplicity-unchained/sign/pset \ - -H "Content-Type: application/json" \ - -d "$SIGN_REQUEST") - -PSET_SIGNED1=$(echo "$PSET_SIGN1_DATA" | jq -r '.pset_hex') - -echo "PSET after first signature: $PSET_SIGNED1" -echo - -echo "==== Step 6: Second Signature (User) ====" -PSET_SIGN2_DATA=$(cargo run --quiet -- tx sign --pset "$PSET_SIGNED1" --secret-key "$USER_SECKEY" --input-index 0 --redeem-script "$REDEEM_SCRIPT") -PSET_SIGNED2=$(echo "$PSET_SIGN2_DATA" | jq -r '.pset') - -echo "PSET after second signature: $PSET_SIGNED2" -echo - -echo "==== Step 7: Finalize PSET ====" -FINALIZE_DATA=$(cargo run --quiet -- tx finalize --pset "$PSET_SIGNED2") -FINAL_TX_HEX=$(echo "$FINALIZE_DATA" | jq -r '.transaction_hex') -TXID=$(echo "$FINALIZE_DATA" | jq -r '.txid') - -echo "Finalized transaction hex: $FINAL_TX_HEX" -echo - - -echo "Submitting raw transaction via Liquid Testnet web API..." -echo -n "Resulting transaction ID is " -TXID=$(curl -X POST "https://blockstream.info/liquidtestnet/api/tx" -d "$FINAL_TX_HEX" 2>/dev/null) -echo "$TXID" -echo -echo "You can view it online at https://blockstream.info/liquidtestnet/tx/$TXID?expand" - -echo diff --git a/cli/scripts/demo_bitcoin.sh b/cli/scripts/demo_bitcoin.sh deleted file mode 100755 index 5ff1d36..0000000 --- a/cli/scripts/demo_bitcoin.sh +++ /dev/null @@ -1,141 +0,0 @@ -#!/bin/bash - -set -e - -# Wait for user confirmation. -pause() { read -p "Press Enter to continue..."; echo; echo; } - -# Wait for transaction to be visible in the mempool. -wait_for_transaction() { - local TXID=$1 - echo "Waiting for transaction to be visible in the mempool..." - for i in {1..30}; do - TX_DATA=$(curl -s "https://mempool.space/testnet4/api/tx/$TXID" 2>/dev/null) - if [ -n "$TX_DATA" ] && [ "$TX_DATA" != "Transaction not found" ]; then - echo "Transaction found in mempool!" - return 0 - fi - sleep 5 - done - - echo "Error: Transaction not found after waiting. Please check the transaction ID and try again." - exit 1 -} - -# Simple Simplicity program that always returns true -PROGRAM="zSQIS29W33fvVt9371bfd+9W33fvVt9371bfd+9W33fvVt93hgGA" -WITNESS="" - -NETWORK="testnet4" - -echo "==== Bitcoin Testnet4 2-of-2 Multisig Demo with Simplicity ====" -echo -echo "This demo will guide you through creating and spending from a" -echo "2-of-2 multisig address where one key is tweaked with a Simplicity program." -echo - -echo "==== Step 0: Get Tweaked Public Key for Simplicity Program ====" -TWEAK_REQUEST=$(jq -n --arg program "$PROGRAM" '{program: $program}') - -TWEAK_RESPONSE=$(curl -s -X POST http://localhost:30431/simplicity-unchained/tweak \ - -H "Content-Type: application/json" \ - -d "$TWEAK_REQUEST") - -# Extract the tweaked public key from the response and set it as COSIGNER_PUBKEY -COSIGNER_PUBKEY=$(echo "$TWEAK_RESPONSE" | jq -r '.tweaked_public_key_hex') - -echo "Tweaked public key (Co-signer): $COSIGNER_PUBKEY" -echo - -echo "==== Step 1: Generate User Keypair ====" -USER_KEYPAIR=$(cargo run --quiet keypair generate) -USER_SECKEY=$(echo "$USER_KEYPAIR" | jq -r '.secret_key') -USER_PUBKEY=$(echo "$USER_KEYPAIR" | jq -r '.public_key') - -echo "User secret key: $USER_SECKEY" -echo "User public key: $USER_PUBKEY" -echo - -echo "==== Step 2: Create 2-of-2 Multisig Address ====" -ADDRESS_DATA=$(cargo run --quiet -- address multisig --pubkey1 $COSIGNER_PUBKEY --pubkey2 $USER_PUBKEY --network $NETWORK) -ADDRESS=$(echo "$ADDRESS_DATA" | jq -r '.address') -REDEEM_SCRIPT=$(echo "$ADDRESS_DATA" | jq -r '.redeem_script') - -echo "Multisig P2WSH address: $ADDRESS" -echo "Redeem script: $REDEEM_SCRIPT" -echo - -echo "==== Step 3: Fund the Address ====" -echo "Please send Bitcoin testnet4 coins to the following address:" -echo "$ADDRESS" -echo "" -echo "You can get testnet4 coins from: https://mempool.space/testnet4/faucet" -echo -echo -n "After funding, please paste the funding transaction ID and press Enter: " -read FUNDING_TXID - -if [ -z "$FUNDING_TXID" ]; then - echo "Error: No transaction ID provided. Exiting." - exit 1 -fi - -echo -echo "Funding transaction ID: $FUNDING_TXID" -echo - -wait_for_transaction "$FUNDING_TXID" -echo - -echo "==== Step 4: Create PSBT ====" -PSBT_CREATE_DATA=$(cargo run --quiet -- btc-tx create -i "$FUNDING_TXID:1" -o "$ADDRESS:4000" --network $NETWORK) -PSBT_HEX=$(echo "$PSBT_CREATE_DATA" | jq -r '.psbt') - -echo "Created PSBT (unsigned): $PSBT_HEX" -echo - -echo "==== Step 5: First Signature (Co-signer) ====" -echo "Calling sign service at http://localhost:30431/simplicity-unchained/sign/psbt..." - -SIGN_REQUEST=$(jq -n \ - --arg psbt "$PSBT_HEX" \ - --arg redeem "$REDEEM_SCRIPT" \ - --arg program "$PROGRAM" \ - --arg witness "$WITNESS" \ - '{psbt_hex: $psbt, redeem_script_hex: $redeem, input_index: 0, program: $program, witness: $witness}') - -PSBT_SIGN1_DATA=$(curl -s -X POST http://localhost:30431/simplicity-unchained/sign/psbt \ - -H "Content-Type: application/json" \ - -d "$SIGN_REQUEST") - -PSBT_SIGNED1=$(echo "$PSBT_SIGN1_DATA" | jq -r '.psbt_hex') - -echo "PSBT after first signature: $PSBT_SIGNED1" -echo - -echo "==== Step 6: Second Signature (User) ====" -PSBT_SIGN2_DATA=$(cargo run --quiet -- btc-tx sign --psbt "$PSBT_SIGNED1" --secret-key "$USER_SECKEY" --input-index 0 --redeem-script "$REDEEM_SCRIPT") -PSBT_SIGNED2=$(echo "$PSBT_SIGN2_DATA" | jq -r '.psbt') - -echo "PSBT after second signature: $PSBT_SIGNED2" -echo - -echo "==== Step 7: Finalize PSBT ====" -FINALIZE_DATA=$(cargo run --quiet -- btc-tx finalize --psbt "$PSBT_SIGNED2") -FINAL_TX_HEX=$(echo "$FINALIZE_DATA" | jq -r '.transaction_hex') -TXID=$(echo "$FINALIZE_DATA" | jq -r '.txid') - -echo "Finalized transaction hex: $FINAL_TX_HEX" -echo - -echo "==== Step 8: Broadcast Transaction ====" -echo "Submitting raw transaction via Bitcoin Testnet4 API..." -echo -n "Resulting transaction ID is " -BROADCAST_TXID=$(curl -X POST "https://mempool.space/testnet4/api/tx" -d "$FINAL_TX_HEX" 2>/dev/null) -echo "$BROADCAST_TXID" -echo -echo "You can view it online at https://mempool.space/testnet4/tx/$BROADCAST_TXID" - -echo -echo "==== Demo Complete! ====" -echo "Successfully created and spent from a 2-of-2 multisig with Simplicity program!" -echo diff --git a/cli/scripts/demo_btc.sh b/cli/scripts/demo_btc.sh new file mode 100755 index 0000000..9b7f1c3 --- /dev/null +++ b/cli/scripts/demo_btc.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Bitcoin policy-v1 demo (experimental; Bitcoin Testnet4 by default) +Usage: ./cli/scripts/demo_btc.sh [address|review|sign|--help] + +Required environment: + POLICY_PROGRAM_BASE64 Reviewed bitcoin-policy-v1 program, not legacy bytes + BASE_SERVICE_PUBLIC_KEY_HEX Independently known untweaked service key + USER_PUBLIC_KEY_HEX Cooperative user public key + RECOVERY_LEAF_HASH_HEX Exact raw recovery TapNodeHash + PSBT_HEX Required for review/sign; one input, full parents + APPROVAL_WITNESS_BASE64 Required for review/sign; set '' if witness-free + USER_SECRET_KEY_FILE Required for sign; private file of user key hex + +Optional: + SERVICE_ORIGIN Default http://127.0.0.1:30431 + NETWORK Default testnet4; must exactly match the service + REDEEM_SCRIPT_HEX Default empty; exact committed script-data bytes + ESPLORA_URL Explicit parent resolver; default strictly offline + +address verifies capabilities/tweak before deriving funding data. +review (default) prepares/evaluates only; it never requests a signature. +sign revalidates a pre-reviewed PSBT/witness, co-signs and finalizes locally. +No faucet, broadcast, recovery spend, legacy endpoint or automatic signing retry. +For multi-input spends use the policy CLI directly. +EOF +} + +mode="${1:-review}" +case "$mode" in + -h|--help|help) usage; exit 0 ;; + address|review|sign) ;; + *) usage >&2; exit 2 ;; +esac +if [[ $# -gt 1 ]]; then + usage >&2 + exit 2 +fi + +command -v cargo >/dev/null +command -v jq >/dev/null +cd "$(dirname "${BASH_SOURCE[0]}")/../.." +su=(cargo run --quiet --locked -p cli -- policy) +service="${SERVICE_ORIGIN:-http://127.0.0.1:30431}" +network="${NETWORK:-testnet4}" +: "${POLICY_PROGRAM_BASE64:?Set a reviewed policy-profile program}" +: "${BASE_SERVICE_PUBLIC_KEY_HEX:?Pin the known untweaked service key}" +: "${USER_PUBLIC_KEY_HEX:?Set the cooperative user key}" +: "${RECOVERY_LEAF_HASH_HEX:?Set the committed raw recovery hash}" + +policy_args=( + --network "$network" --program "$POLICY_PROGRAM_BASE64" + --redeem-script-hex "${REDEEM_SCRIPT_HEX:-}" +) +binding_args=( + "${policy_args[@]}" + --base-public-key "$BASE_SERVICE_PUBLIC_KEY_HEX" + --user-pubkey "$USER_PUBLIC_KEY_HEX" + --user-leaf-hash-hex "$RECOVERY_LEAF_HASH_HEX" +) + +if [[ "$mode" == address ]]; then + "${su[@]}" capabilities --network "$network" --service "$service" >&2 + "${su[@]}" tweak "${policy_args[@]}" \ + --base-public-key "$BASE_SERVICE_PUBLIC_KEY_HEX" --service "$service" >&2 + "${su[@]}" address "${binding_args[@]}" + exit 0 +fi + +: "${PSBT_HEX:?Supply an already constructed, pre-reviewed policy PSBT}" +if [[ ! ${APPROVAL_WITNESS_BASE64+x} ]]; then + echo "Set APPROVAL_WITNESS_BASE64 explicitly (empty only for witness-free policies)" >&2 + exit 2 +fi +resolver=(--offline) +if [[ -n "${ESPLORA_URL:-}" ]]; then + resolver=(--esplora-url "$ESPLORA_URL") +fi +tx_args=("${binding_args[@]}" --psbt "$PSBT_HEX" --input-index 0 "${resolver[@]}") +prepared=$("${su[@]}" prepare "${tx_args[@]}" --service "$service") +echo "$prepared" >&2 +if [[ "$(jq -er '.context.input_count' <<<"$prepared")" != 1 ]]; then + echo "Demo requires exactly one input; use the policy CLI for multiple inputs" >&2 + exit 2 +fi +"${su[@]}" approval-hash "${tx_args[@]}" >&2 +"${su[@]}" evaluate "${tx_args[@]}" --witness "$APPROVAL_WITNESS_BASE64" \ + --service "$service" >&2 +if [[ "$mode" == review ]]; then + echo "$prepared" + exit 0 +fi + +: "${USER_SECRET_KEY_FILE:?Provide a private file containing the user secret key hex}" +signed=$("${su[@]}" sign "${tx_args[@]}" --witness "$APPROVAL_WITNESS_BASE64" \ + --service "$service") +psbt=$(jq -er '.psbt_hex' <<<"$signed") +tx_args=("${binding_args[@]}" --psbt "$psbt" --input-index 0 "${resolver[@]}") +user_signed=$("${su[@]}" user-sign "${tx_args[@]}" --secret-key-file "$USER_SECRET_KEY_FILE") +psbt=$(jq -er '.psbt_hex' <<<"$user_signed") +tx_args=("${binding_args[@]}" --psbt "$psbt" --input-index 0 "${resolver[@]}") +"${su[@]}" finalize "${tx_args[@]}" diff --git a/cli/src/commands/address/mod.rs b/cli/src/commands/address/mod.rs deleted file mode 100644 index 5c9758a..0000000 --- a/cli/src/commands/address/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod multisig; diff --git a/cli/src/commands/address/multisig.rs b/cli/src/commands/address/multisig.rs deleted file mode 100644 index 95ea22e..0000000 --- a/cli/src/commands/address/multisig.rs +++ /dev/null @@ -1,122 +0,0 @@ -use anyhow::{Context, Result}; -use serde_json::json; - -use hal_simplicity::simplicity::bitcoin; -use hal_simplicity::simplicity::elements::{AddressParams, bitcoin::PublicKey}; - -use simplicity_unchained_core::utils::{ - generate_2of2_multisig_address_bitcoin, generate_2of2_multisig_address_elements, -}; - -pub fn execute(pubkey1: &str, pubkey2: &str, network: &str) -> Result<()> { - let pk1_bytes = hex::decode(pubkey1).context("Failed to decode pubkey1")?; - let pk2_bytes = hex::decode(pubkey2).context("Failed to decode pubkey2")?; - - let pk1 = PublicKey::from_slice(&pk1_bytes).context("Invalid pubkey1")?; - let pk2 = PublicKey::from_slice(&pk2_bytes).context("Invalid pubkey2")?; - - let pubkeys = vec![pk1, pk2]; - - if let Ok(network) = ElementsNetwork::try_from(network) { - let output = execute_over_elements(&pubkeys, network)?; - - println!("{}", serde_json::to_string_pretty(&output)?); - - return Ok(()); - } - - if let Ok(network) = BitcoinNetwork::try_from(network) { - let output = execute_over_bitcoin(&pubkeys, network)?; - - println!("{}", serde_json::to_string_pretty(&output)?); - - return Ok(()); - } - - Err(anyhow::anyhow!( - "Unsupported network: {}. Supported networks are elements, liquid, liquid_testnet, bitcoin, testnet, testnet4, signet, regtest", - network - )) -} - -fn execute_over_elements( - pubkeys: &[PublicKey], - network: ElementsNetwork, -) -> Result { - let params = match network { - ElementsNetwork::Elements => &AddressParams::ELEMENTS, - ElementsNetwork::Liquid => &AddressParams::LIQUID, - ElementsNetwork::LiquidTestnet => &AddressParams::LIQUID_TESTNET, - }; - - let (address, redeem_script) = generate_2of2_multisig_address_elements(&pubkeys, params)?; - - Ok(json!({ - "address": address.to_string(), - "redeem_script": hex::encode(&redeem_script.to_bytes()), - })) -} - -fn execute_over_bitcoin( - pubkeys: &[PublicKey], - network: BitcoinNetwork, -) -> Result { - let network = match network { - BitcoinNetwork::Bitcoin => bitcoin::Network::Bitcoin, - BitcoinNetwork::Testnet => bitcoin::Network::Testnet, - BitcoinNetwork::Testnet4 => bitcoin::Network::Testnet4, - BitcoinNetwork::Signet => bitcoin::Network::Signet, - BitcoinNetwork::Regtest => bitcoin::Network::Regtest, - }; - - let (address, redeem_script) = generate_2of2_multisig_address_bitcoin(&pubkeys, network)?; - - Ok(json!({ - "address": address.to_string(), - "redeem_script": hex::encode(&redeem_script.to_bytes()), - })) -} - -#[derive(Debug)] -enum ElementsNetwork { - Elements, - Liquid, - LiquidTestnet, -} - -impl TryFrom<&str> for ElementsNetwork { - type Error = anyhow::Error; - - fn try_from(s: &str) -> Result { - match s { - "elements" => Ok(ElementsNetwork::Elements), - "liquid" => Ok(ElementsNetwork::Liquid), - "liquid_testnet" => Ok(ElementsNetwork::LiquidTestnet), - _ => Err(anyhow::anyhow!("Unsupported elements network: {}", s)), - } - } -} - -#[derive(Debug)] -enum BitcoinNetwork { - Bitcoin, - Testnet, - Testnet4, - Signet, - Regtest, -} - -impl TryFrom<&str> for BitcoinNetwork { - type Error = anyhow::Error; - - fn try_from(s: &str) -> Result { - match s { - "bitcoin" => Ok(BitcoinNetwork::Bitcoin), - "testnet" => Ok(BitcoinNetwork::Testnet), - "testnet4" => Ok(BitcoinNetwork::Testnet4), - "signet" => Ok(BitcoinNetwork::Signet), - "regtest" => Ok(BitcoinNetwork::Regtest), - _ => Err(anyhow::anyhow!("Unsupported bitcoin network: {}", s)), - } - } -} diff --git a/cli/src/commands/btc_tx/create.rs b/cli/src/commands/btc_tx/create.rs deleted file mode 100644 index 8b4f39f..0000000 --- a/cli/src/commands/btc_tx/create.rs +++ /dev/null @@ -1,191 +0,0 @@ -use anyhow::{Context, Result, anyhow}; - -use hal_simplicity::simplicity::bitcoin; - -use bitcoin::{ - Address, Amount, Network, OutPoint, Transaction, TxIn, TxOut, psbt::Psbt, script::ScriptBuf, -}; - -use std::str::FromStr; -use std::thread; -use std::time::Duration; - -#[derive(serde::Deserialize)] -struct BlockstreamTx { - vout: Vec, -} - -#[derive(serde::Deserialize)] -struct BlockstreamVout { - value: Option, - scriptpubkey: Option, -} - -fn fetch_tx_output(txid: &str, vout: u32, network: &str) -> Result { - let api_url = match network { - "bitcoin" => format!("https://blockstream.info/api/tx/{}", txid), - "testnet" => format!("https://blockstream.info/testnet/api/tx/{}", txid), - "testnet4" => format!("https://mempool.space/testnet4/api/tx/{}", txid), - _ => return Err(anyhow!("Cannot fetch transaction for network: {}", network)), - }; - - const MAX_RETRIES: u32 = 10; - const RETRY_DELAY_SECS: u64 = 3; - - let mut last_error = None; - - for attempt in 1..=MAX_RETRIES { - match reqwest::blocking::get(&api_url) - .context("Failed to fetch transaction") - .and_then(|resp| resp.text().context("Failed to read response")) - .and_then(|response| { - serde_json::from_str::(&response) - .context("Failed to parse transaction JSON") - }) { - Ok(tx_data) => { - let output = tx_data - .vout - .get(vout as usize) - .ok_or_else(|| anyhow!("Output {} not found in transaction", vout))?; - - let value = output - .value - .ok_or_else(|| anyhow!("Output {} has no value", vout))?; - let scriptpubkey_str = output - .scriptpubkey - .as_ref() - .ok_or_else(|| anyhow!("Output {} has no scriptpubkey", vout))?; - - let script_bytes = - hex::decode(scriptpubkey_str).context("Invalid scriptpubkey hex")?; - - return Ok(TxOut { - value: Amount::from_sat(value), - script_pubkey: ScriptBuf::from_bytes(script_bytes), - }); - } - Err(e) => { - last_error = Some(e); - if attempt < MAX_RETRIES { - eprintln!( - "Attempt {}/{} failed, RPC is not responding, retrying in {} seconds...", - attempt, MAX_RETRIES, RETRY_DELAY_SECS - ); - thread::sleep(Duration::from_secs(RETRY_DELAY_SECS)); - } - } - } - } - - Err(anyhow!( - "Server is down or unreachable after {} attempts: {}", - MAX_RETRIES, - last_error.unwrap() - )) -} - -fn get_network_kind(network: &str) -> Result { - match network { - "bitcoin" => Ok(Network::Bitcoin), - "testnet" => Ok(Network::Testnet), - "testnet4" => Ok(Network::Testnet4), - _ => Err(anyhow!( - "Unsupported network '{}'. Supported networks are: bitcoin, testnet.", - network - )), - } -} - -pub fn execute(inputs: &[String], outputs: &[String], network: &str) -> Result<()> { - let network_type = get_network_kind(network)?; - - // Parse inputs (txid:vout) - let mut tx_inputs = Vec::new(); - for input_str in inputs { - let parts: Vec<&str> = input_str.split(':').collect(); - if parts.len() != 2 { - return Err(anyhow!( - "Invalid input format. Expected txid:vout, got: {}", - input_str - )); - } - - let txid = - bitcoin::Txid::from_str(parts[0]).context(format!("Invalid txid: {}", parts[0]))?; - let vout: u32 = parts[1] - .parse() - .context(format!("Invalid vout: {}", parts[1]))?; - - tx_inputs.push(TxIn { - previous_output: OutPoint::new(txid, vout), - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: Default::default(), - }); - } - - // Parse outputs (address:value) - let mut tx_outputs = Vec::new(); - for output_str in outputs { - let parts: Vec<&str> = output_str.split(':').collect(); - if parts.len() != 2 { - return Err(anyhow!( - "Invalid output format. Expected address:value, got: {}", - output_str - )); - } - - let value: u64 = parts[1] - .parse() - .context(format!("Invalid value: {}", parts[1]))?; - - let address = Address::from_str(parts[0]) - .context(format!("Invalid address: {}", parts[0]))? - .require_network(network_type) - .context(format!( - "Address {} is not valid for network {}", - parts[0], network - ))?; - - tx_outputs.push(TxOut { - value: Amount::from_sat(value), - script_pubkey: address.script_pubkey(), - }); - } - - let tx = Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: tx_inputs, - output: tx_outputs, - }; - - let mut psbt = Psbt::from_unsigned_tx(tx)?; - - // Populate witness UTXO for each input by fetching from the blockchain - for (i, input_str) in inputs.iter().enumerate() { - let parts: Vec<&str> = input_str.split(':').collect(); - let txid_str = parts[0]; - let vout: u32 = parts[1].parse().unwrap(); - - match fetch_tx_output(txid_str, vout, network) { - Ok(prev_output) => { - psbt.inputs[i].witness_utxo = Some(prev_output); - } - Err(e) => { - return Err(anyhow!("Could not fetch UTXO for input {}: {}", i, e)); - } - } - } - - let output = serde_json::json!({ - "psbt": hex::encode(psbt.serialize()), - "inputs": psbt.inputs.len(), - "outputs": psbt.outputs.len(), - "network": network, - }); - - println!("{}", serde_json::to_string_pretty(&output)?); - - Ok(()) -} diff --git a/cli/src/commands/btc_tx/finalize.rs b/cli/src/commands/btc_tx/finalize.rs deleted file mode 100644 index 6d6eb49..0000000 --- a/cli/src/commands/btc_tx/finalize.rs +++ /dev/null @@ -1,114 +0,0 @@ -use anyhow::{Context, Result}; -use hal_simplicity::simplicity::bitcoin::{PublicKey, psbt::Psbt}; -use serde_json::json; - -pub fn execute(psbt_hex: &str) -> Result<()> { - let psbt_bytes = hex::decode(psbt_hex).context("Failed to decode PSBT hex")?; - let psbt: Psbt = Psbt::deserialize(&psbt_bytes).context("Failed to deserialize PSBT")?; - - let mut tx = psbt.clone().extract_tx_unchecked_fee_rate(); - - // For each input, build the witness from partial signatures - for (i, input) in psbt.inputs.iter().enumerate() { - if input.partial_sigs.is_empty() { - return Err(anyhow::anyhow!("Input {} has no partial signatures", i)); - } - - let witness_script = input - .witness_script - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Input {} missing witness script", i))?; - - // For 2-of-2 multisig, build witness: OP_0 - // We need to order the signatures according to the public keys in the redeem script - if input.partial_sigs.len() < 2 { - return Err(anyhow::anyhow!( - "Input {} requires 2 signatures but only has {}", - i, - input.partial_sigs.len() - )); - } - - // Extract public keys from the witness script (2-of-2 multisig format: OP_2 OP_2 OP_CHECKMULTISIG) - let script_bytes = witness_script.as_bytes(); - let mut pubkeys = Vec::new(); - let mut i_byte = 0; - while i_byte < script_bytes.len() { - if script_bytes[i_byte] == 33 { - // Compressed public key length - if i_byte + 33 < script_bytes.len() { - let pk_bytes = &script_bytes[i_byte + 1..i_byte + 34]; - if let Ok(pk) = PublicKey::from_slice(pk_bytes) { - pubkeys.push(pk); - } - i_byte += 34; - } else { - break; - } - } else { - i_byte += 1; - } - } - - if pubkeys.len() != 2 { - return Err(anyhow::anyhow!( - "Input {} witness script does not contain exactly 2 public keys", - i - )); - } - - // Order signatures according to the public keys in the redeem script - let sig1 = input - .partial_sigs - .get(&pubkeys[0]) - .ok_or_else(|| anyhow::anyhow!("Missing signature for first public key"))?; - let sig2 = input - .partial_sigs - .get(&pubkeys[1]) - .ok_or_else(|| anyhow::anyhow!("Missing signature for second public key"))?; - - // Serialize signatures with sighash type - let mut sig1_bytes = sig1.signature.serialize_der().to_vec(); - sig1_bytes.push(sig1.sighash_type.to_u32() as u8); - - let mut sig2_bytes = sig2.signature.serialize_der().to_vec(); - sig2_bytes.push(sig2.sighash_type.to_u32() as u8); - - tx.input[i].witness.push(vec![]); // OP_0 for multisig - tx.input[i].witness.push(sig1_bytes); - tx.input[i].witness.push(sig2_bytes); - tx.input[i].witness.push(witness_script.to_bytes()); - } - - // Verify all inputs have witnesses - let all_finalized = tx.input.iter().all(|input| !input.witness.is_empty()); - - if !all_finalized { - return Err(anyhow::anyhow!( - "Not all inputs are finalized - transaction may be missing signatures" - )); - } - - let witnesses: Vec<_> = tx - .input - .iter() - .map(|input| { - input - .witness - .iter() - .map(|w| hex::encode(w)) - .collect::>() - }) - .collect(); - - let output = json!({ - "transaction_hex": hex::encode(hal_simplicity::simplicity::bitcoin::consensus::serialize(&tx)), - "txid": tx.compute_txid().to_string(), - "witnesses": witnesses, - "finalized": all_finalized, - }); - - println!("{}", serde_json::to_string_pretty(&output)?); - - Ok(()) -} diff --git a/cli/src/commands/btc_tx/mod.rs b/cli/src/commands/btc_tx/mod.rs deleted file mode 100644 index 4ef0875..0000000 --- a/cli/src/commands/btc_tx/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod create; -pub mod finalize; -pub mod sign; diff --git a/cli/src/commands/btc_tx/sign.rs b/cli/src/commands/btc_tx/sign.rs deleted file mode 100644 index dc93c63..0000000 --- a/cli/src/commands/btc_tx/sign.rs +++ /dev/null @@ -1,99 +0,0 @@ -use anyhow::{Context, Result}; -use hal_simplicity::simplicity::bitcoin::{ - EcdsaSighashType, NetworkKind, PrivateKey, PublicKey, hashes::Hash, psbt::Psbt, script::Script, - sighash::SighashCache, -}; -use hal_simplicity::simplicity::elements::secp256k1_zkp::{Message, Secp256k1, SecretKey}; -use serde_json::json; - -pub fn execute( - psbt_hex: &str, - secret_key_hex: &str, - input_index: usize, - redeem_script_hex: &str, -) -> Result<()> { - let psbt_bytes = hex::decode(psbt_hex).context("Failed to decode PSBT hex")?; - let mut psbt: Psbt = Psbt::deserialize(&psbt_bytes).context("Failed to deserialize PSBT")?; - - if input_index >= psbt.inputs.len() { - return Err(anyhow::anyhow!( - "Input index {} out of bounds (PSBT has {} inputs)", - input_index, - psbt.inputs.len() - )); - } - - let secret_key_bytes = - hex::decode(secret_key_hex).context("Failed to decode secret key hex")?; - let secret_key = SecretKey::from_slice(&secret_key_bytes).context("Invalid secret key")?; - - let redeem_script_bytes = - hex::decode(redeem_script_hex).context("Failed to decode redeem script hex")?; - let redeem_script = Script::from_bytes(&redeem_script_bytes).to_owned(); - - let secp = Secp256k1::new(); - - let public_key = PublicKey::from_private_key( - &secp, - &PrivateKey { - compressed: true, - network: NetworkKind::Main, - inner: secret_key, - }, - ); - - let psbt_input = &psbt.inputs[input_index]; - let prev_value = psbt_input - .witness_utxo - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing witness UTXO for input {}", input_index))? - .value; - - // Clone psbt to extract transaction, since extract_tx consumes it - let tx = psbt.clone().extract_tx_unchecked_fee_rate(); - - // Compute sighash for P2WSH (SegWit v0) - let mut sighash_cache = SighashCache::new(&tx); - let sighash = sighash_cache - .p2wsh_signature_hash( - input_index, - &redeem_script, - prev_value, - EcdsaSighashType::All, - ) - .context("Failed to compute sighash")?; - - let msg = Message::from_digest(sighash.to_byte_array()); - let signature = secp.sign_ecdsa(&msg, &secret_key); - - // Convert to bitcoin's Signature type for PSBT - let bitcoin_sig = hal_simplicity::simplicity::bitcoin::ecdsa::Signature { - signature, - sighash_type: EcdsaSighashType::All, - }; - - // Add signature to PSBT - let input = &mut psbt.inputs[input_index]; - input.partial_sigs.insert(public_key, bitcoin_sig); - - if input.witness_script.is_none() { - input.witness_script = Some(redeem_script); - } - - let partial_sigs_count = psbt.inputs[input_index].partial_sigs.len(); - - let mut sig_bytes = signature.serialize_der().to_vec(); - sig_bytes.push(EcdsaSighashType::All.to_u32() as u8); - - let output = json!({ - "psbt": hex::encode(psbt.serialize()), - "signature_hex": hex::encode(&sig_bytes), - "public_key_hex": hex::encode(public_key.to_bytes()), - "input_index": input_index, - "partial_sigs_count": partial_sigs_count, - }); - - println!("{}", serde_json::to_string_pretty(&output)?); - - Ok(()) -} diff --git a/cli/src/commands/keypair/generate.rs b/cli/src/commands/keypair/generate.rs index a142a2d..b3f011f 100644 --- a/cli/src/commands/keypair/generate.rs +++ b/cli/src/commands/keypair/generate.rs @@ -6,8 +6,8 @@ pub fn execute() -> Result<()> { let (secret_key, public_key) = generate_keypair(); let output = json!({ - "secret_key": hex::encode(&secret_key.secret_bytes()), - "public_key": hex::encode(&public_key.to_bytes()), + "secret_key": hex::encode(secret_key.secret_bytes()), + "public_key": hex::encode(public_key.to_bytes()), "compressed": public_key.compressed }); diff --git a/cli/src/commands/liquid_policy.rs b/cli/src/commands/liquid_policy.rs new file mode 100644 index 0000000..2a9319f --- /dev/null +++ b/cli/src/commands/liquid_policy.rs @@ -0,0 +1,371 @@ +//! New, proof-verified Liquid policy CLI. No legacy command alias. +use anyhow::{Context as _, Result, ensure}; +use clap::Subcommand; +use serde::Deserialize; +use serde_json::{Value, json}; +use simplicity::base64::{Engine, engine::general_purpose::STANDARD}; +use simplicity_unchained_policy::liquid::{ + self as policy, + elements::{ + Address, + encode::{deserialize, serialize}, + pset::PartiallySignedTransaction as Pset, + secp256k1_zkp::{PublicKey, Secp256k1, SecretKey, XOnlyPublicKey, rand::rngs::OsRng}, + taproot::TapNodeHash, + }, +}; +use std::{fs, io::Read, path::PathBuf, str::FromStr, time::Duration}; + +#[derive(Subcommand)] +pub enum Command { + /// Print this binary's canonical liquid-policy-v1 registry + Registry, + /// Check service profile/network/base-key identity against a local request + Capabilities { + #[arg(long)] + request_file: PathBuf, + #[arg(long, default_value = "http://127.0.0.1:30431")] + service: String, + }, + /// Derive a Liquid profile-bound service key locally + Tweak { + #[arg(long)] + request_file: PathBuf, + }, + /// Reconstruct the cooperative funding address, optionally confidential + Address { + #[arg(long)] + request_file: PathBuf, + }, + /// Generate a local receiving blinding key; protect the private output + GenerateBlindingKey, + /// Build/blind a PSET with full parents and browser/local unblinding keys + Create { + #[arg(long)] + request_file: PathBuf, + }, + /// Verify all evidence/proofs/signatures and prepare a local context + Prepare { + #[arg(long)] + request_file: PathBuf, + }, + /// Compute input-specific and transaction-wide approval hashes locally + ApprovalHash { + #[arg(long)] + request_file: PathBuf, + }, + /// Execute a witnessed policy locally without signing + Evaluate { + #[arg(long)] + request_file: PathBuf, + }, + /// Request service signing and independently verify the returned PSET/signature + Sign { + #[arg(long)] + request_file: PathBuf, + #[arg(long, default_value = "http://127.0.0.1:30431")] + service: String, + }, + /// Reverify disclosures/service signature and add a local user signature + UserSign { + #[arg(long)] + request_file: PathBuf, + #[arg(long)] + secret_key_file: PathBuf, + }, + /// Reverify disclosures and both signatures for every input, then finalize + Finalize { + #[arg(long)] + request_file: PathBuf, + }, +} + +#[derive(Debug, Deserialize)] +#[serde(deny_unknown_fields)] +struct Request { + #[serde(default = "profile")] + profile: String, + #[serde(default)] + profile_hash: Option, + #[serde(default)] + network: policy::Network, + program: String, + #[serde(default)] + redeem_script_hex: String, + base_public_key_hex: String, + #[serde(default)] + pset_hex: Option, + #[serde(default)] + input_index: usize, + #[serde(default)] + user_pubkey: Option, + #[serde(default)] + user_leaf_hash_hex: Option, + #[serde(default)] + blinding_public_key_hex: Option, + #[serde(default)] + witness: Option, +} +fn profile() -> String { + policy::PROFILE_NAME.into() +} +fn read(path: PathBuf) -> Result { + let size = fs::metadata(&path)?.len(); + ensure!( + size <= (simplicity_unchained_policy::MAX_REQUEST_BYTES * 2) as u64, + "request exceeds evidence limit" + ); + Ok(serde_json::from_slice(&fs::read(path)?)?) +} +fn key(s: &str) -> Result { + match s.len() { + 64 => Ok(XOnlyPublicKey::from_str(s)?), + 66 => Ok(PublicKey::from_str(s)?.x_only_public_key().0), + _ => anyhow::bail!("expected compressed/x-only public key"), + } +} +impl Request { + fn binding(&self) -> Result<(Vec, Vec, XOnlyPublicKey)> { + ensure!( + self.profile == policy::PROFILE_NAME, + "explicit Liquid profile mismatch" + ); + ensure!( + self.profile_hash + .as_ref() + .is_none_or(|h| *h == hex::encode(policy::profile_hash())), + "Liquid fingerprint mismatch" + ); + Ok(( + STANDARD.decode(&self.program)?, + hex::decode(&self.redeem_script_hex)?, + key(&self.base_public_key_hex)?, + )) + } + fn recovery(&self) -> Result { + let bytes: [u8; 32] = hex::decode( + self.user_leaf_hash_hex + .as_ref() + .context("missing user_leaf_hash_hex")?, + )? + .try_into() + .map_err(|_| anyhow::anyhow!("recovery hash must be 32 bytes"))?; + Ok(TapNodeHash::from_byte_array(bytes)) + } + fn prepare(&self) -> Result<(Pset, policy::PreparedPolicy)> { + let (program, data, base) = self.binding()?; + let pset_hex = self.pset_hex.as_ref().context("missing pset_hex")?; + ensure!( + pset_hex.len() <= simplicity_unchained_policy::MAX_REQUEST_BYTES * 2, + "PSET exceeds profile limit" + ); + let pset: Pset = deserialize(&hex::decode(pset_hex)?)?; + let prepared = policy::PreparedPolicy::new( + &program, + &pset, + self.input_index, + &data, + self.network, + base, + key(self.user_pubkey.as_ref().context("missing user_pubkey")?)?, + self.recovery()?, + )?; + Ok((pset, prepared)) + } + fn witness(&self) -> Result> { + Ok(STANDARD.decode( + self.witness + .as_ref() + .context("witness required; use empty string for no witness")?, + )?) + } + fn remote(&self) -> Value { + json!({"profile_hash":hex::encode(policy::profile_hash()),"network":self.network.name(),"program":self.program,"redeem_script_hex":self.redeem_script_hex,"pset_hex":self.pset_hex,"input_index":self.input_index,"user_pubkey":self.user_pubkey,"user_leaf_hash_hex":self.user_leaf_hash_hex,"witness":self.witness}) + } +} +fn http(service: &str, operation: &str, body: Option) -> Result { + let origin = reqwest::Url::parse(service)?; + ensure!( + matches!(origin.scheme(), "http" | "https") + && origin.username().is_empty() + && origin.password().is_none() + && origin.query().is_none() + && origin.fragment().is_none() + && origin.path() == "/", + "service must be a bare HTTP(S) origin without credentials/query" + ); + let client = reqwest::blocking::Client::builder() + .timeout(Duration::from_secs(10)) + .redirect(reqwest::redirect::Policy::none()) + .build()?; + let url = format!( + "{}/simplicity-unchained/policy/liquid/{operation}", + service.trim_end_matches('/') + ); + let response = if let Some(body) = body { + client.post(url).json(&body).send()? + } else { + client.get(url).send()? + }; + let status = response.status(); + let mut bytes = Vec::new(); + response + .take((simplicity_unchained_policy::MAX_REQUEST_BYTES + 1) as u64) + .read_to_end(&mut bytes)?; + ensure!( + bytes.len() <= simplicity_unchained_policy::MAX_REQUEST_BYTES, + "service response exceeds limit" + ); + ensure!( + status.is_success(), + "service failed ({status}): {}", + String::from_utf8_lossy(&bytes) + ); + Ok(serde_json::from_slice(&bytes)?) +} +fn capabilities(r: &Request, service: &str) -> Result { + let (_, _, base) = r.binding()?; + let v = http(service, "capabilities", None)?; + ensure!( + v["profile"] == policy::PROFILE_NAME + && v["profile_hash"] == hex::encode(policy::profile_hash()), + "service profile/fingerprint mismatch" + ); + ensure!( + v["network"] == r.network.name() + && v["genesis_hash"] == hex::encode(r.network.genesis_hash().to_byte_array()), + "service network/genesis mismatch" + ); + ensure!( + v["base_public_key_hex"] == base.to_string() && v["manifest"] == policy::manifest(), + "service base key/manifest mismatch" + ); + Ok(v) +} + +pub fn execute(command: Command) -> Result<()> { + let value = match command { + Command::Registry => { + json!({"profile":policy::PROFILE_NAME,"profile_hash":hex::encode(policy::profile_hash()),"manifest":policy::manifest()}) + } + Command::GenerateBlindingKey => { + let secret = SecretKey::new(&mut OsRng); + json!({"blinding_secret_key_hex":hex::encode(secret.secret_bytes()),"blinding_public_key_hex":PublicKey::from_secret_key(&Secp256k1::new(),&secret).to_string()}) + } + Command::Create { request_file } => { + let request: policy::CreateRequest = read(request_file)?; + let pset = policy::create_pset(&request)?; + json!({"pset_hex":hex::encode(serialize(&pset)),"network":request.network.name(),"inputs":pset.inputs().len(),"outputs":pset.outputs().len()}) + } + Command::Capabilities { + request_file, + service, + } => capabilities(&read(request_file)?, &service)?, + Command::Tweak { request_file } => { + let r: Request = read(request_file)?; + let (program, data, base) = r.binding()?; + let cmr = policy::policy_cmr(&program)?.to_byte_array(); + json!({"profile":policy::PROFILE_NAME,"profile_hash":hex::encode(policy::profile_hash()),"network":r.network.name(),"genesis_hash":hex::encode(r.network.genesis_hash().to_byte_array()),"cmr":hex::encode(cmr),"signing_commitment":hex::encode(policy::signing_commitment(cmr,&data,r.network)?),"public_key_hex":policy::tweak_public_key(base,cmr,&data,r.network)?.to_string(),"base_public_key_hex":base.to_string()}) + } + Command::Address { request_file } => { + let r: Request = read(request_file)?; + let (program, data, base) = r.binding()?; + let output = policy::cooperative_output( + base, + policy::policy_cmr(&program)?.to_byte_array(), + &data, + r.network, + key(r.user_pubkey.as_ref().context("missing user_pubkey")?)?, + r.recovery()?, + )?; + let blinding = r + .blinding_public_key_hex + .as_deref() + .map(PublicKey::from_str) + .transpose()?; + let address = + Address::from_script(&output.script_pubkey, blinding, r.network.address_params()) + .context("cannot encode funding address")?; + json!({"address":address.to_string(),"script_pubkey_hex":hex::encode(output.script_pubkey.as_bytes()),"profile":policy::PROFILE_NAME,"profile_hash":hex::encode(policy::profile_hash()),"network":r.network.name()}) + } + Command::Prepare { request_file } | Command::ApprovalHash { request_file } => { + let r: Request = read(request_file)?; + json!({"context":r.prepare()?.1.summary()}) + } + Command::Evaluate { request_file } => { + let r: Request = read(request_file)?; + let (_, prepared) = r.prepare()?; + json!({"accepted":true,"context":prepared.summary(),"execution_cost_milliweight":prepared.evaluate_default(&r.witness()?)?}) + } + Command::Sign { + request_file, + service, + } => { + let r: Request = read(request_file)?; + let (_, before) = r.prepare()?; + let cost = before.evaluate_default(&r.witness()?)?; + capabilities(&r, &service)?; + let response = http(&service, "sign", Some(r.remote()))?; + let encoded = response["pset_hex"] + .as_str() + .context("missing signed PSET")?; + ensure!( + encoded.len() <= simplicity_unchained_policy::MAX_REQUEST_BYTES * 2, + "signed PSET exceeds limit" + ); + let signed: Pset = deserialize(&hex::decode(encoded)?)?; + before.context().revalidate(&signed)?; + ensure!( + response["context"] == serde_json::to_value(before.summary())?, + "service context changed" + ); + ensure!( + response["execution_cost_milliweight"] == cost, + "service execution cost changed" + ); + ensure!( + response["public_key_hex"] + == before.context().cooperative().service_pubkey.to_string() + && response["input_index"] == r.input_index, + "service signer/index changed" + ); + let sig = signed.inputs()[r.input_index] + .tap_script_sigs + .get(&( + before.context().cooperative().service_pubkey, + before.context().cooperative().leaf_hash, + )) + .context("missing verified service signature")?; + ensure!( + response["signature_hex"] == hex::encode(sig.sig.as_ref()), + "service signature response mismatch" + ); + response + } + Command::UserSign { + request_file, + secret_key_file, + } => { + let r: Request = read(request_file)?; + let (mut pset, prepared) = r.prepare()?; + ensure!( + pset.inputs()[r.input_index].tap_script_sigs.contains_key(&( + prepared.context().cooperative().service_pubkey, + prepared.context().cooperative().leaf_hash + )), + "missing verified service signature" + ); + let secret = SecretKey::from_str(fs::read_to_string(secret_key_file)?.trim())?; + let sig = policy::add_signature(prepared.context(), &mut pset, secret, false)?; + json!({"pset_hex":hex::encode(serialize(&pset)),"signature_hex":hex::encode(sig.sig.as_ref()),"public_key_hex":prepared.context().user_pubkey().to_string(),"input_index":r.input_index,"context":prepared.summary()}) + } + Command::Finalize { request_file } => { + let r: Request = read(request_file)?; + let (pset, prepared) = r.prepare()?; + let tx = policy::finalize(prepared.context(), &pset)?; + json!({"transaction_hex":hex::encode(serialize(&tx)),"txid":tx.txid().to_string(),"finalized":true,"inputs":tx.input.len(),"outputs":tx.output.len()}) + } + }; + println!("{}", serde_json::to_string_pretty(&value)?); + Ok(()) +} diff --git a/cli/src/commands/mod.rs b/cli/src/commands/mod.rs index 02f3b04..35fd2b4 100644 --- a/cli/src/commands/mod.rs +++ b/cli/src/commands/mod.rs @@ -1,4 +1,3 @@ -pub mod address; -pub mod btc_tx; pub mod keypair; -pub mod tx; +pub mod liquid_policy; +pub mod policy; diff --git a/cli/src/commands/policy.rs b/cli/src/commands/policy.rs new file mode 100644 index 0000000..7c1b68e --- /dev/null +++ b/cli/src/commands/policy.rs @@ -0,0 +1,895 @@ +mod finalize; + +use std::{collections::HashMap, io::Read, path::PathBuf, time::Duration}; + +use anyhow::{Context as _, Result, bail, ensure}; +use clap::{Args, Subcommand}; +use reqwest::{ + Url, + blocking::{Client, Response}, + redirect::Policy, +}; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::{Value, json}; +use simplicity::{ + base64::prelude::{BASE64_STANDARD, Engine as _}, + bitcoin::{ + Address, Network, Psbt, PublicKey, TapLeafHash, TapNodeHash, Transaction, XOnlyPublicKey, + consensus::{deserialize, serialize}, + hashes::Hash, + secp256k1::{Keypair, Message, Secp256k1, SecretKey, schnorr}, + taproot::LeafVersion, + }, +}; +use simplicity_unchained_core::policy::{PreparedPolicy, policy_cmr}; +use simplicity_unchained_policy::{ + ContextSummary, Limits, MAX_EXECUTION_COST_MILLIWEIGHT, MAX_INPUTS, MAX_OUTPUTS, + MAX_REQUEST_BYTES, PROFILE_NAME, cooperative_output, genesis_hash, manifest, profile_hash, + signing_commitment, tweak_public_key, +}; + +const POLICY_PATH: &str = "/simplicity-unchained/policy/bitcoin"; +const MAX_BODY_BYTES: usize = MAX_REQUEST_BYTES; + +#[derive(Subcommand)] +pub enum PolicyCommand { + /// Derive the exact policy-bound funding address locally before funding + Address { + #[command(flatten)] + policy: ProgramArgs, + #[arg(long)] + base_public_key: String, + #[arg(long)] + user_pubkey: String, + #[arg(long)] + user_leaf_hash_hex: String, + }, + /// Build a canonical Bitcoin CSV recovery leaf before deriving the address + BuildCsvLeaf { + #[arg(long)] + user_pubkey: String, + #[arg(long)] + timelock: u16, + }, + /// Check the exact compiled profile, manifest and configured service network + Capabilities { + #[command(flatten)] + identity: IdentityArgs, + #[command(flatten)] + service: ServiceArgs, + }, + /// Derive the profile-bound service key before funding + Tweak { + #[command(flatten)] + policy: ProgramArgs, + #[command(flatten)] + service: ServiceArgs, + /// Optionally pin the service's known untweaked x-only or compressed key + #[arg(long)] + base_public_key: Option, + }, + /// Verify full parent evidence and compare the service's prepared context locally + Prepare { + #[command(flatten)] + transaction: TransactionArgs, + #[command(flatten)] + service: ServiceArgs, + }, + /// Evaluate a witnessed policy without requesting a transaction signature + Evaluate { + #[command(flatten)] + transaction: TransactionArgs, + #[command(flatten)] + service: ServiceArgs, + /// Standard-base64 encoded Simplicity witness (an empty string is allowed) + #[arg(long)] + witness: String, + }, + /// Compute J53/J54 locally from verified parents and a known base service key + ApprovalHash { + #[command(flatten)] + transaction: TransactionArgs, + }, + /// Revalidate and evaluate the exact policy, then verify the service's signature + Sign { + #[command(flatten)] + transaction: TransactionArgs, + #[command(flatten)] + service: ServiceArgs, + /// Standard-base64 encoded Simplicity witness (an empty string is allowed) + #[arg(long)] + witness: String, + }, + /// Add the user's cooperative signature after verifying the policy context + UserSign { + #[command(flatten)] + transaction: TransactionArgs, + /// File containing the user's 32-byte secret key as hex; keep it private + #[arg(long)] + secret_key_file: PathBuf, + }, + /// Verify the policy binding and both signatures for every input, then finalize + Finalize { + #[command(flatten)] + transaction: TransactionArgs, + }, +} + +#[derive(Args)] +pub struct IdentityArgs { + /// Explicit policy profile; no legacy/profile fallback is permitted + #[arg(long, default_value = PROFILE_NAME)] + profile: String, + /// Expected registry fingerprint; defaults to this binary's compiled fingerprint + #[arg(long)] + profile_hash: Option, + /// Expected Bitcoin network; must exactly match the configured service + #[arg(long, default_value = "testnet4")] + network: String, +} + +#[derive(Args)] +pub struct ServiceArgs { + /// Service origin, without an endpoint path + #[arg(long, default_value = "http://127.0.0.1:30431")] + service: String, +} + +#[derive(Args)] +pub struct ProgramArgs { + #[command(flatten)] + identity: IdentityArgs, + /// Standard-base64 encoded program compiled for bitcoin-policy-v1, not legacy + #[arg(long)] + program: String, + /// Exact committed script-data bytes; omission means an empty script + #[arg(long, default_value = "")] + redeem_script_hex: String, +} + +#[derive(Args)] +pub struct TransactionArgs { + #[command(flatten)] + policy: ProgramArgs, + /// Hex serialized PSBT; full parent transactions are required for every input + #[arg(long)] + psbt: String, + #[arg(long)] + input_index: usize, + /// Cooperative user's compressed or x-only public key, in hex + #[arg(long)] + user_pubkey: String, + /// Exact 32-byte recovery TapNodeHash, in raw-byte hex (not explorer byte order) + #[arg(long)] + user_leaf_hash_hex: String, + /// Known untweaked service public key, compressed or x-only hex + #[arg(long)] + base_public_key: String, + /// Explicit Esplora API base; fetch missing full parents from /tx//raw + #[arg(long, conflicts_with = "offline")] + esplora_url: Option, + /// Forbid parent fetching; all non_witness_utxo evidence must already be present + #[arg(long)] + offline: bool, +} + +#[derive(Serialize)] +struct ProgramRequest { + profile_hash: String, + network: String, + program: String, + redeem_script_hex: String, +} + +#[derive(Serialize)] +struct TransactionRequest { + #[serde(flatten)] + policy: ProgramRequest, + psbt_hex: String, + input_index: usize, + user_pubkey: String, + user_leaf_hash_hex: String, + #[serde(skip_serializing_if = "Option::is_none")] + witness: Option, +} + +#[derive(Deserialize, Serialize)] +struct CapabilitiesResponse { + profile: String, + profile_hash: String, + network: String, + genesis_hash: String, + base_public_key_hex: String, + manifest: Value, + limits: Value, + provenance: String, +} + +#[derive(Deserialize, Serialize)] +struct TweakResponse { + profile: String, + profile_hash: String, + network: String, + genesis_hash: String, + cmr: String, + signing_commitment: String, + public_key_hex: String, + base_public_key_hex: String, +} + +#[derive(Deserialize, Serialize)] +struct PrepareResponse { + context: ContextSummary, +} + +#[derive(Deserialize, Serialize)] +struct EvaluateResponse { + accepted: bool, + context: ContextSummary, + execution_cost_milliweight: u64, +} + +#[derive(Deserialize, Serialize)] +struct SignResponse { + psbt_hex: String, + signature_hex: String, + public_key_hex: String, + input_index: usize, + context: ContextSummary, + execution_cost_milliweight: u64, +} + +struct ServiceClient { + client: Client, + base: String, +} + +impl ServiceClient { + fn new(args: &ServiceArgs) -> Result { + let origin = validate_url(&args.service)?; + ensure!( + origin.path() == "/", + "--service must be an origin without an endpoint path" + ); + Ok(Self { + client: http_client()?, + base: format!("{}{}", origin.as_str().trim_end_matches('/'), POLICY_PATH), + }) + } + + fn capabilities( + &self, + identity: &IdentityArgs, + expected_key: Option, + ) -> Result { + let response = self + .client + .get(format!("{}/capabilities", self.base)) + .send() + .context("Failed to fetch policy capabilities")?; + let result: CapabilitiesResponse = decode_response(response)?; + check_identity( + identity, + &result.profile, + &result.profile_hash, + &result.network, + &result.genesis_hash, + )?; + let compiled_manifest = manifest(); + ensure!( + result.manifest == serde_json::to_value(&compiled_manifest)?, + "Service policy manifest differs from the compiled registry" + ); + let limits: Limits = serde_json::from_value(result.limits.clone()) + .context("Invalid service policy limits")?; + ensure!( + limits == compiled_manifest.limits, + "Service policy limits differ from the compiled profile" + ); + ensure!( + result.provenance == "parent-txid-verified", + "Service policy verification provenance mismatch" + ); + let base_key = parse_public_key(&result.base_public_key_hex)?; + if let Some(expected_key) = expected_key { + ensure!( + base_key == expected_key, + "Service base public key does not match --base-public-key" + ); + } + Ok(result) + } + + fn post(&self, operation: &str, body: &T) -> Result { + let body = serde_json::to_vec(body)?; + ensure!( + body.len() <= MAX_BODY_BYTES, + "Policy request exceeds the 2 MiB body limit" + ); + let response = self + .client + .post(format!("{}/{}", self.base, operation)) + .header(reqwest::header::CONTENT_TYPE, "application/json") + .body(body) + .send() + .with_context(|| format!("Policy {operation} request failed"))?; + decode_response(response) + } +} + +pub fn execute(command: PolicyCommand) -> Result<()> { + match run(command) { + Ok(output) => { + println!("{}", serde_json::to_string_pretty(&output)?); + Ok(()) + } + Err(error) => { + println!("{}", json!({ "error": format!("{error:#}") })); + Err(error) + } + } +} + +fn run(command: PolicyCommand) -> Result { + match command { + PolicyCommand::Address { + policy, + base_public_key, + user_pubkey, + user_leaf_hash_hex, + } => { + let (network, program, script) = policy.validate()?; + let cmr = policy_cmr(&program)?; + let output = cooperative_output( + parse_public_key(&base_public_key)?, + cmr.to_byte_array(), + &script, + network, + parse_public_key(&user_pubkey)?, + parse_recovery_hash(&user_leaf_hash_hex)?, + )?; + Ok(json!({ + "profile": PROFILE_NAME, + "profile_hash": hex::encode(profile_hash()), + "network": policy.identity.network, + "genesis_hash": hex::encode(genesis_hash(network)?), + "cmr": hex::encode(cmr.to_byte_array()), + "signing_commitment": hex::encode(signing_commitment(cmr.to_byte_array(), &script, network)?), + "address": Address::from_script(&output.script_pubkey, network)?.to_string(), + "script_pubkey_hex": hex::encode(output.script_pubkey.as_bytes()), + })) + } + PolicyCommand::BuildCsvLeaf { + user_pubkey, + timelock, + } => { + ensure!(timelock > 0, "Recovery timelock must be at least one block"); + let script = simplicity::bitcoin::script::Builder::new() + .push_int(i64::from(timelock)) + .push_opcode(simplicity::bitcoin::opcodes::all::OP_CSV) + .push_opcode(simplicity::bitcoin::opcodes::all::OP_DROP) + .push_x_only_key(&parse_public_key(&user_pubkey)?) + .push_opcode(simplicity::bitcoin::opcodes::all::OP_CHECKSIG) + .into_script(); + Ok(json!({ + "script": hex::encode(script.as_bytes()), + "leaf_hash": hex::encode(TapLeafHash::from_script(&script, LeafVersion::TapScript).to_byte_array()), + })) + } + PolicyCommand::Capabilities { identity, service } => { + identity.validate()?; + let result = ServiceClient::new(&service)?.capabilities(&identity, None)?; + Ok(serde_json::to_value(result)?) + } + PolicyCommand::Tweak { + policy, + service, + base_public_key, + } => { + let (network, program, script) = policy.validate()?; + let expected_key = base_public_key + .as_deref() + .map(parse_public_key) + .transpose()?; + let client = ServiceClient::new(&service)?; + let capabilities = client.capabilities(&policy.identity, expected_key)?; + let base_key = parse_public_key(&capabilities.base_public_key_hex)?; + let cmr = policy_cmr(&program).context("Invalid policy-profile program")?; + let cmr_bytes = cmr.to_byte_array(); + let expected_commitment = signing_commitment(cmr_bytes, &script, network)?; + let expected_tweaked = tweak_public_key(base_key, cmr_bytes, &script, network)?; + let result: TweakResponse = client.post("tweak", &policy.request())?; + check_identity( + &policy.identity, + &result.profile, + &result.profile_hash, + &result.network, + &result.genesis_hash, + )?; + ensure!( + result.cmr == hex::encode(cmr_bytes) + && result.signing_commitment == hex::encode(expected_commitment) + && result.public_key_hex == expected_tweaked.to_string() + && parse_public_key(&result.base_public_key_hex)? == base_key, + "Service tweak does not match the locally derived policy commitment/key" + ); + Ok(serde_json::to_value(result)?) + } + PolicyCommand::ApprovalHash { transaction } => { + let local = LocalTransaction::new(&transaction)?; + Ok(serde_json::to_value(PrepareResponse { + context: local.prepared.summary(), + })?) + } + PolicyCommand::Prepare { + transaction, + service, + } => { + let local = LocalTransaction::new(&transaction)?; + let client = checked_service(&service, &transaction, &local)?; + let result: PrepareResponse = + client.post("prepare", &local.request(&transaction, None))?; + check_context(&result.context, &local.prepared.summary())?; + Ok(serde_json::to_value(result)?) + } + PolicyCommand::Evaluate { + transaction, + service, + witness, + } => { + let witness_bytes = decode_base64(&witness, "witness")?; + let local = LocalTransaction::new(&transaction)?; + let evaluated = local.prepared.evaluate_default(&witness_bytes)?; + let client = checked_service(&service, &transaction, &local)?; + let result: EvaluateResponse = + client.post("evaluate", &local.request(&transaction, Some(witness)))?; + ensure!(result.accepted, "Service did not accept the policy"); + check_context(&result.context, &local.prepared.summary())?; + check_cost(result.execution_cost_milliweight)?; + ensure!( + result.execution_cost_milliweight == u64::from(evaluated.cost_milliweight), + "Service execution cost differs from local policy evaluation" + ); + Ok(serde_json::to_value(result)?) + } + PolicyCommand::Sign { + transaction, + service, + witness, + } => { + let witness_bytes = decode_base64(&witness, "witness")?; + let local = LocalTransaction::new(&transaction)?; + let evaluated = local.prepared.evaluate_default(&witness_bytes)?; + let client = checked_service(&service, &transaction, &local)?; + let result: SignResponse = + client.post("sign", &local.request(&transaction, Some(witness)))?; + check_context(&result.context, &local.prepared.summary())?; + check_cost(result.execution_cost_milliweight)?; + ensure!( + result.execution_cost_milliweight == u64::from(evaluated.cost_milliweight), + "Service execution cost differs from local policy evaluation" + ); + local.verify_signature(&transaction, &result)?; + Ok(serde_json::to_value(result)?) + } + PolicyCommand::UserSign { + transaction, + secret_key_file, + } => { + let mut local = LocalTransaction::new(&transaction)?; + let secret = std::fs::read_to_string(secret_key_file) + .context("Failed to read user secret key file")?; + let secret = hex::decode(secret.trim()).context("Invalid user secret key hex")?; + let key = Keypair::from_secret_key( + &Secp256k1::new(), + &SecretKey::from_slice(&secret).context("Invalid user secret key")?, + ); + let context = local.prepared.context(); + ensure!( + key.x_only_public_key().0 == context.user_pubkey(), + "User secret key does not match the committed user public key" + ); + let signature = Secp256k1::new().sign_schnorr( + &Message::from_digest(context.cooperative_sighash_default()), + &key, + ); + let input = &mut local.psbt.inputs[transaction.input_index]; + input.tap_script_sigs.insert( + (context.user_pubkey(), context.cooperative_leaf_hash()), + simplicity::bitcoin::taproot::Signature { + signature, + sighash_type: simplicity::bitcoin::TapSighashType::Default, + }, + ); + input.tap_scripts.insert( + context.control_block().clone(), + (context.cooperative_script().clone(), LeafVersion::TapScript), + ); + Ok(json!({ + "psbt_hex": hex::encode(local.psbt.serialize()), + "input_index": transaction.input_index, + "context": local.prepared.summary(), + })) + } + PolicyCommand::Finalize { transaction } => { + let local = LocalTransaction::new(&transaction)?; + let tx = finalize::finalize(&local.psbt)?; + Ok(json!({ + "transaction_hex": hex::encode(serialize(&tx)), + "txid": tx.compute_txid().to_string(), + "inputs": tx.input.len(), + "outputs": tx.output.len(), + "finalized": true, + })) + } + } +} + +impl IdentityArgs { + fn validate(&self) -> Result { + ensure!( + self.profile == PROFILE_NAME, + "Unsupported policy profile '{}'; expected {PROFILE_NAME}", + self.profile + ); + if let Some(expected_hash) = &self.profile_hash { + ensure!( + hex::decode(expected_hash).context("Invalid profile fingerprint hex")? + == profile_hash(), + "Policy fingerprint does not match this binary's compiled registry" + ); + } + match self.network.as_str() { + "bitcoin" => Ok(Network::Bitcoin), + "testnet" => Ok(Network::Testnet), + "testnet4" => Ok(Network::Testnet4), + _ => bail!("Unsupported policy network; expected bitcoin, testnet, or testnet4"), + } + } +} + +impl ProgramArgs { + fn validate(&self) -> Result<(Network, Vec, Vec)> { + let network = self.identity.validate()?; + let program = decode_base64(&self.program, "program")?; + ensure!(!program.is_empty(), "Policy program cannot be empty"); + policy_cmr(&program).context("Invalid policy-profile program")?; + let script = hex::decode(&self.redeem_script_hex).context("Invalid redeem script hex")?; + Ok((network, program, script)) + } + + fn request(&self) -> ProgramRequest { + ProgramRequest { + profile_hash: hex::encode(profile_hash()), + network: self.identity.network.clone(), + program: self.program.clone(), + redeem_script_hex: self.redeem_script_hex.clone(), + } + } +} + +struct LocalTransaction { + psbt: Psbt, + prepared: PreparedPolicy, + base_key: XOnlyPublicKey, +} + +impl LocalTransaction { + fn new(args: &TransactionArgs) -> Result { + let (network, program, script) = args.policy.validate()?; + let base_key = parse_public_key(&args.base_public_key)?; + let user_key = parse_public_key(&args.user_pubkey)?; + let recovery = parse_recovery_hash(&args.user_leaf_hash_hex)?; + let mut psbt = parse_psbt(&args.psbt)?; + let resolver = if args.offline { + None + } else { + args.esplora_url.as_deref() + }; + populate_parents(&mut psbt, resolver)?; + let prepared = PreparedPolicy::new( + &program, + &psbt, + args.input_index, + &script, + network, + base_key, + user_key, + recovery, + ) + .context("Failed to construct verified policy context")?; + Ok(Self { + psbt, + prepared, + base_key, + }) + } + + fn request(&self, args: &TransactionArgs, witness: Option) -> TransactionRequest { + TransactionRequest { + policy: args.policy.request(), + psbt_hex: hex::encode(self.psbt.serialize()), + input_index: args.input_index, + user_pubkey: args.user_pubkey.clone(), + user_leaf_hash_hex: args.user_leaf_hash_hex.clone(), + witness, + } + } + + fn verify_signature(&self, args: &TransactionArgs, result: &SignResponse) -> Result<()> { + ensure!( + result.input_index == args.input_index, + "Service signed an unexpected input index" + ); + let context = self.prepared.context(); + ensure!( + result.public_key_hex == context.service_pubkey().to_string(), + "Service signature public key differs from the reconstructed policy key" + ); + let signature = schnorr::Signature::from_slice( + &hex::decode(&result.signature_hex).context("Invalid service signature hex")?, + ) + .context("Service must return a 64-byte SIGHASH_DEFAULT signature")?; + Secp256k1::verification_only() + .verify_schnorr( + &signature, + &Message::from_digest(context.cooperative_sighash_default()), + &context.service_pubkey(), + ) + .context("Service returned an invalid cooperative signature")?; + let returned_psbt = parse_psbt(&result.psbt_hex)?; + ensure!( + returned_psbt.unsigned_tx == self.psbt.unsigned_tx + && returned_psbt.inputs.len() == self.psbt.inputs.len(), + "Service changed the transaction" + ); + for (index, (returned, original)) in returned_psbt + .inputs + .iter() + .zip(&self.psbt.inputs) + .enumerate() + { + ensure!( + returned.witness_utxo == original.witness_utxo + && returned.non_witness_utxo == original.non_witness_utxo, + "Service changed parent evidence for input {index}" + ); + } + let input = &returned_psbt.inputs[args.input_index]; + let leaf_hash = + TapLeafHash::from_script(context.cooperative_script(), LeafVersion::TapScript); + let stored = input + .tap_script_sigs + .get(&(context.service_pubkey(), leaf_hash)) + .context("Returned PSBT does not contain the verified service signature")?; + ensure!( + stored.signature == signature + && stored.sighash_type == simplicity::bitcoin::TapSighashType::Default, + "Returned PSBT contains a different service signature/sighash type" + ); + ensure!( + input.tap_scripts.get(context.control_block()) + == Some(&( + context.cooperative_script().to_owned(), + LeafVersion::TapScript + )), + "Returned PSBT is missing the reconstructed cooperative leaf/control block" + ); + Ok(()) + } +} + +fn checked_service( + service: &ServiceArgs, + transaction: &TransactionArgs, + local: &LocalTransaction, +) -> Result { + let client = ServiceClient::new(service)?; + client.capabilities(&transaction.policy.identity, Some(local.base_key))?; + Ok(client) +} + +fn check_identity( + expected: &IdentityArgs, + profile: &str, + fingerprint: &str, + network: &str, + genesis: &str, +) -> Result<()> { + let expected_network = expected.validate()?; + ensure!(profile == PROFILE_NAME, "Service policy profile mismatch"); + ensure!( + fingerprint == hex::encode(profile_hash()), + "Service policy fingerprint mismatch" + ); + ensure!( + network == expected.network, + "Service policy network mismatch" + ); + ensure!( + genesis == hex::encode(genesis_hash(expected_network)?), + "Service genesis hash mismatch" + ); + Ok(()) +} + +fn check_context(remote: &ContextSummary, local: &ContextSummary) -> Result<()> { + ensure!( + remote == local, + "Service policy context/digests differ from the locally verified context" + ); + Ok(()) +} + +fn check_cost(cost: u64) -> Result<()> { + ensure!( + cost <= u64::from(MAX_EXECUTION_COST_MILLIWEIGHT), + "Service reported a cost above the policy execution limit" + ); + Ok(()) +} + +fn parse_recovery_hash(encoded: &str) -> Result { + TapNodeHash::from_slice(&hex::decode(encoded).context("Invalid recovery leaf hash hex")?) + .context("Recovery leaf hash must be exactly 32 bytes") +} + +fn parse_public_key(encoded: &str) -> Result { + let bytes = hex::decode(encoded).context("Invalid public key hex")?; + match bytes.len() { + 32 => XOnlyPublicKey::from_slice(&bytes).context("Invalid x-only public key"), + 33 => PublicKey::from_slice(&bytes) + .map(|key| key.inner.x_only_public_key().0) + .context("Invalid compressed public key"), + _ => bail!("Public key must be 32-byte x-only or 33-byte compressed hex"), + } +} + +fn parse_psbt(encoded: &str) -> Result { + let bytes = hex::decode(encoded).context("Invalid PSBT hex")?; + ensure!( + bytes.len() <= MAX_BODY_BYTES, + "PSBT exceeds the policy body limit" + ); + Psbt::deserialize(&bytes).context("Invalid serialized PSBT") +} + +fn decode_base64(encoded: &str, label: &str) -> Result> { + ensure!( + encoded.len() <= MAX_BODY_BYTES, + "Policy {label} exceeds the body limit" + ); + BASE64_STANDARD + .decode(encoded) + .with_context(|| format!("Invalid standard-base64 policy {label}")) +} + +fn http_client() -> Result { + Client::builder() + .timeout(Duration::from_secs(30)) + .redirect(Policy::none()) + .build() + .context("Failed to create HTTP client") +} + +fn validate_url(value: &str) -> Result { + let url = Url::parse(value).context("Invalid HTTP URL")?; + ensure!( + matches!(url.scheme(), "http" | "https") + && url.host_str().is_some() + && url.username().is_empty() + && url.password().is_none() + && url.query().is_none() + && url.fragment().is_none(), + "Expected an HTTP(S) URL without credentials, query, or fragment" + ); + Ok(url) +} + +fn read_body(response: Response) -> Result<(reqwest::StatusCode, Vec)> { + let status = response.status(); + let mut body = Vec::new(); + response + .take((MAX_BODY_BYTES + 1) as u64) + .read_to_end(&mut body) + .context("Failed to read HTTP response body")?; + ensure!( + body.len() <= MAX_BODY_BYTES, + "HTTP response exceeds the 2 MiB policy limit (status {status})" + ); + Ok((status, body)) +} + +fn decode_response(response: Response) -> Result { + let (status, body) = read_body(response)?; + ensure!( + status.is_success(), + "Policy service returned HTTP {status}: {}", + String::from_utf8_lossy(&body) + ); + serde_json::from_slice(&body).context("Invalid policy service response JSON/shape") +} + +fn populate_parents(psbt: &mut Psbt, esplora: Option<&str>) -> Result<()> { + ensure!( + !psbt.inputs.is_empty() && psbt.inputs.len() == psbt.unsigned_tx.input.len(), + "Invalid PSBT input count" + ); + ensure!( + psbt.inputs.len() <= MAX_INPUTS && psbt.unsigned_tx.output.len() <= MAX_OUTPUTS, + "Policy profile accepts at most eight inputs and eight outputs" + ); + let esplora = esplora.map(validate_url).transpose()?; + let client = esplora.as_ref().map(|_| http_client()).transpose()?; + let mut parents = HashMap::<_, Transaction>::new(); + for (index, (input, txin)) in psbt.inputs.iter().zip(&psbt.unsigned_tx.input).enumerate() { + if let Some(parent) = &input.non_witness_utxo { + ensure!( + parent.compute_txid() == txin.previous_output.txid, + "Input {index} parent transaction txid mismatch" + ); + if let Some(known) = parents.insert(txin.previous_output.txid, parent.clone()) { + ensure!( + known == *parent, + "Conflicting full parent transaction evidence" + ); + } + } + } + for (index, (input, txin)) in psbt + .inputs + .iter_mut() + .zip(&psbt.unsigned_tx.input) + .enumerate() + { + let outpoint = txin.previous_output; + if input.non_witness_utxo.is_none() { + if let Some(parent) = parents.get(&outpoint.txid) { + input.non_witness_utxo = Some(parent.clone()); + } else if let (Some(esplora), Some(client)) = (&esplora, &client) { + let url = format!( + "{}/tx/{}/raw", + esplora.as_str().trim_end_matches('/'), + outpoint.txid + ); + let response = client + .get(url) + .send() + .with_context(|| format!("Failed to fetch parent for input {index}"))?; + let (status, bytes) = read_body(response)?; + ensure!( + status.is_success(), + "Esplora returned HTTP {status} for input {index}: {}", + String::from_utf8_lossy(&bytes) + ); + input.non_witness_utxo = + Some(deserialize::(&bytes).with_context(|| { + format!("Invalid parent transaction for input {index}") + })?); + } else { + bail!( + "Input {index} missing non_witness_utxo full parent evidence in offline mode; provide --esplora-url or populate every parent" + ); + } + } + let parent = input.non_witness_utxo.as_ref().unwrap(); + ensure!( + parent.compute_txid() == outpoint.txid, + "Input {index} parent transaction txid mismatch" + ); + let prevout = parent + .output + .get(outpoint.vout as usize) + .with_context(|| format!("Input {index} parent output index out of bounds"))?; + if let Some(witness_utxo) = &input.witness_utxo { + ensure!( + witness_utxo == prevout, + "Input {index} witness_utxo conflicts with verified parent output" + ); + } else { + input.witness_utxo = Some(prevout.clone()); + } + parents.insert(outpoint.txid, parent.clone()); + } + Ok(()) +} diff --git a/cli/src/commands/policy/finalize.rs b/cli/src/commands/policy/finalize.rs new file mode 100644 index 0000000..0dab5f1 --- /dev/null +++ b/cli/src/commands/policy/finalize.rs @@ -0,0 +1,117 @@ +use anyhow::{Context, Result, anyhow, ensure}; +use simplicity::bitcoin::{ + Psbt, TapLeafHash, Transaction, TxOut, Witness, XOnlyPublicKey, + hashes::Hash, + psbt, + secp256k1::{Message, Secp256k1}, + sighash::{Prevouts, SighashCache}, + taproot::LeafVersion, +}; +use simplicity_unchained_core::utils::extract_pubkeys_from_p2tr_multisig_leaf_btc; + +pub(super) fn finalize(psbt: &Psbt) -> Result { + let mut tx = psbt.unsigned_tx.clone(); + ensure!(!tx.input.is_empty(), "Transaction has no inputs"); + ensure!( + tx.input.len() == psbt.inputs.len(), + "PSBT input count does not match transaction" + ); + let prevouts = psbt + .inputs + .iter() + .enumerate() + .map(|(i, input)| { + input + .witness_utxo + .clone() + .ok_or_else(|| anyhow!("Input {} missing witness_utxo", i)) + }) + .collect::>>()?; + for (i, input) in psbt.inputs.iter().enumerate() { + tx.input[i].witness = finalize_p2tr(&tx, i, input, &prevouts)?; + } + Ok(tx) +} + +fn finalize_p2tr( + tx: &Transaction, + i: usize, + input: &psbt::Input, + prevouts: &[TxOut], +) -> Result { + let spent_script = &prevouts[i].script_pubkey; + ensure!( + spent_script.is_p2tr(), + "Input {} spent output is not P2TR", + i + ); + ensure!( + tx.input[i].script_sig.is_empty(), + "Input {} native P2TR script_sig must be empty", + i + ); + let output_key = XOnlyPublicKey::from_slice(&spent_script.as_bytes()[2..]) + .with_context(|| format!("Input {} invalid Taproot output key", i))?; + let secp = Secp256k1::verification_only(); + let mut candidate = None; + for (control_block, (leaf_script, leaf_version)) in &input.tap_scripts { + if *leaf_version != LeafVersion::TapScript + || control_block.leaf_version != *leaf_version + || !control_block.verify_taproot_commitment(&secp, output_key, leaf_script) + { + continue; + } + let Ok(pubkeys) = extract_pubkeys_from_p2tr_multisig_leaf_btc(leaf_script, i) else { + continue; + }; + ensure!( + candidate.is_none(), + "Input {} has ambiguous committed multisig leaves", + i + ); + candidate = Some((control_block, leaf_script, pubkeys)); + } + let (control_block, leaf_script, pubkeys) = + candidate.ok_or_else(|| anyhow!("Input {} has no committed canonical multisig leaf", i))?; + let leaf_hash = TapLeafHash::from_script(leaf_script, LeafVersion::TapScript); + + let signature = |position| { + input + .tap_script_sigs + .get(&(pubkeys[position], leaf_hash)) + .ok_or_else(|| { + anyhow!( + "Input {} missing tap_script_sig for multisig key {}", + i, + position + ) + }) + }; + let cosigner_sig = signature(0)?; + let user_sig = signature(1)?; + let mut cache = SighashCache::new(tx); + for (key, signature) in pubkeys.iter().zip([cosigner_sig, user_sig]) { + let sighash = cache + .taproot_script_spend_signature_hash( + i, + &Prevouts::All(prevouts), + leaf_hash, + signature.sighash_type, + ) + .with_context(|| format!("Input {} failed to compute signature sighash", i))?; + secp.verify_schnorr( + &signature.signature, + &Message::from_digest(sighash.to_byte_array()), + key, + ) + .with_context(|| format!("Input {} invalid tap_script_sig for {}", i, key))?; + } + + let mut witness = Witness::new(); + witness.push(user_sig.serialize()); + witness.push(cosigner_sig.serialize()); + witness.push(leaf_script.as_bytes()); + witness.push(control_block.serialize()); + + Ok(witness) +} diff --git a/cli/src/commands/tx/create.rs b/cli/src/commands/tx/create.rs deleted file mode 100644 index ca4567a..0000000 --- a/cli/src/commands/tx/create.rs +++ /dev/null @@ -1,226 +0,0 @@ -use anyhow::{Context, Result, anyhow}; - -use hal_simplicity::simplicity::elements; - -use elements::{ - Address, AddressParams, AssetId, OutPoint, Transaction, TxIn, TxOut, confidential, - encode::serialize, pset::PartiallySignedTransaction, -}; - -use std::str::FromStr; -use std::thread; -use std::time::Duration; - -#[derive(serde::Deserialize)] -struct BlockstreamTx { - vout: Vec, -} - -#[derive(serde::Deserialize)] -struct BlockstreamVout { - asset: Option, - value: Option, - scriptpubkey: Option, -} - -fn fetch_tx_output(txid: &str, vout: u32, network: &str) -> Result { - let api_url = match network { - "liquid" => format!("https://blockstream.info/liquid/api/tx/{}", txid), - "liquid_testnet" => format!("https://blockstream.info/liquidtestnet/api/tx/{}", txid), - _ => return Err(anyhow!("Cannot fetch transaction for network: {}", network)), - }; - - const MAX_RETRIES: u32 = 10; - const RETRY_DELAY_SECS: u64 = 3; - - let mut last_error = None; - - for attempt in 1..=MAX_RETRIES { - match reqwest::blocking::get(&api_url) - .context("Failed to fetch transaction") - .and_then(|resp| resp.text().context("Failed to read response")) - .and_then(|response| { - serde_json::from_str::(&response) - .context("Failed to parse transaction JSON") - }) { - Ok(tx_data) => { - let output = tx_data - .vout - .get(vout as usize) - .ok_or_else(|| anyhow!("Output {} not found in transaction", vout))?; - - let asset_str = output - .asset - .as_ref() - .ok_or_else(|| anyhow!("Output {} has no asset", vout))?; - let value = output - .value - .ok_or_else(|| anyhow!("Output {} has no value", vout))?; - let scriptpubkey_str = output - .scriptpubkey - .as_ref() - .ok_or_else(|| anyhow!("Output {} has no scriptpubkey", vout))?; - - let asset = AssetId::from_str(asset_str).context("Invalid asset ID")?; - let script_bytes = - hex::decode(scriptpubkey_str).context("Invalid scriptpubkey hex")?; - - return Ok(TxOut { - asset: confidential::Asset::Explicit(asset), - value: confidential::Value::Explicit(value), - nonce: confidential::Nonce::Null, - script_pubkey: elements::script::Script::from(script_bytes), - witness: elements::TxOutWitness::default(), - }); - } - Err(e) => { - last_error = Some(e); - if attempt < MAX_RETRIES { - eprintln!( - "Attempt {}/{} failed, RPC is not responding, retrying in {} seconds...", - attempt, MAX_RETRIES, RETRY_DELAY_SECS - ); - thread::sleep(Duration::from_secs(RETRY_DELAY_SECS)); - } - } - } - } - - Err(anyhow!( - "Server is down or unreachable after {} attempts: {}", - MAX_RETRIES, - last_error.unwrap() - )) -} - -fn get_network_params(network: &str) -> Result<&'static AddressParams> { - match network { - "elements" => Ok(&AddressParams::ELEMENTS), - "liquid" => Ok(&AddressParams::LIQUID), - "liquid_testnet" => Ok(&AddressParams::LIQUID_TESTNET), - _ => Err(anyhow!( - "Unsupported network '{}'. Supported networks are: elements, liquid, liquid_testnet.", - network - )), - } -} - -fn get_default_asset(network: &str) -> &'static str { - match network { - "liquid" => "6f0279e9ed041c3d710a9f57d0c02928416460c4b722ae3457a11eec381c526d", - "liquid_testnet" => "144c654344aa716d6f3abcc1ca90e5641e4e2a7f633bc09fe3baf64585819a49", - _ => "0000000000000000000000000000000000000000000000000000000000000000", - } -} - -pub fn execute( - inputs: &[String], - outputs: &[String], - asset_id: Option<&str>, - network: &str, -) -> Result<()> { - let params = get_network_params(network)?; - - let asset = if let Some(asset_str) = asset_id { - AssetId::from_str(asset_str).context("Invalid asset ID")? - } else { - AssetId::from_str(get_default_asset(network)).context("Invalid default asset ID")? - }; - - // Parse inputs (txid:vout) - let mut tx_inputs = Vec::new(); - for input_str in inputs { - let parts: Vec<&str> = input_str.split(':').collect(); - if parts.len() != 2 { - return Err(anyhow!( - "Invalid input format. Expected txid:vout, got: {}", - input_str - )); - } - - let txid = - elements::Txid::from_str(parts[0]).context(format!("Invalid txid: {}", parts[0]))?; - let vout: u32 = parts[1] - .parse() - .context(format!("Invalid vout: {}", parts[1]))?; - - tx_inputs.push(TxIn { - previous_output: OutPoint::new(txid, vout), - is_pegin: false, - script_sig: elements::script::Script::new(), - sequence: elements::Sequence::MAX, - asset_issuance: elements::AssetIssuance::default(), - witness: elements::TxInWitness::default(), - }); - } - - // Parse outputs (address:value) - let mut tx_outputs = Vec::new(); - for output_str in outputs { - let parts: Vec<&str> = output_str.split(':').collect(); - if parts.len() != 2 { - return Err(anyhow!( - "Invalid output format. Expected address:value, got: {}", - output_str - )); - } - - let value: u64 = parts[1] - .parse() - .context(format!("Invalid value: {}", parts[1]))?; - - let script_pubkey = match parts[0] { - "fee" => elements::script::Script::new(), - address_str => { - let address = Address::parse_with_params(address_str, params) - .context(format!("Invalid address: {}", address_str))?; - address.script_pubkey() - } - }; - - tx_outputs.push(TxOut { - asset: confidential::Asset::Explicit(asset), - value: confidential::Value::Explicit(value), - nonce: confidential::Nonce::Null, - script_pubkey, - witness: elements::TxOutWitness::default(), - }); - } - - let tx = Transaction { - version: 2, - lock_time: elements::LockTime::ZERO, - input: tx_inputs, - output: tx_outputs, - }; - - let mut pset = PartiallySignedTransaction::from_tx(tx); - - // Populate witness UTXO for each input by fetching from the blockchain - for (i, input_str) in inputs.iter().enumerate() { - let parts: Vec<&str> = input_str.split(':').collect(); - let txid_str = parts[0]; - let vout: u32 = parts[1].parse().unwrap(); - - match fetch_tx_output(txid_str, vout, network) { - Ok(prev_output) => { - pset.inputs_mut()[i].witness_utxo = Some(prev_output); - } - Err(e) => { - return Err(anyhow!("Could not fetch UTXO for input {}: {}", i, e)); - } - } - } - - let output = serde_json::json!({ - "pset": hex::encode(serialize(&pset)), - "inputs": pset.inputs().len(), - "outputs": pset.outputs().len(), - "network": network, - "asset": asset.to_string() - }); - - println!("{}", serde_json::to_string_pretty(&output)?); - - Ok(()) -} diff --git a/cli/src/commands/tx/finalize.rs b/cli/src/commands/tx/finalize.rs deleted file mode 100644 index af0eecf..0000000 --- a/cli/src/commands/tx/finalize.rs +++ /dev/null @@ -1,123 +0,0 @@ -use anyhow::{Context, Result}; -use hal_simplicity::simplicity::elements::{ - bitcoin::PublicKey, - encode::{deserialize, serialize}, - pset::PartiallySignedTransaction, -}; -use serde_json::json; - -pub fn execute(pset_hex: &str) -> Result<()> { - let pset_bytes = hex::decode(pset_hex).context("Failed to decode PSET hex")?; - let pset: PartiallySignedTransaction = - deserialize(&pset_bytes).context("Failed to deserialize PSET")?; - - let mut tx = pset - .extract_tx() - .context("Failed to extract transaction from PSET")?; - - // For each input, build the witness from partial signatures - for (i, input) in pset.inputs().iter().enumerate() { - if input.partial_sigs.is_empty() { - return Err(anyhow::anyhow!("Input {} has no partial signatures", i)); - } - - let witness_script = input - .witness_script - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Input {} missing witness script", i))?; - - // For 2-of-2 multisig, build witness: OP_0 - // We need to order the signatures according to the public keys in the redeem script - if input.partial_sigs.len() < 2 { - return Err(anyhow::anyhow!( - "Input {} requires 2 signatures but only has {}", - i, - input.partial_sigs.len() - )); - } - - // Extract public keys from the witness script (2-of-2 multisig format: OP_2 OP_2 OP_CHECKMULTISIG) - let script_bytes = witness_script.as_bytes(); - let mut pubkeys = Vec::new(); - let mut i_byte = 0; - while i_byte < script_bytes.len() { - if script_bytes[i_byte] == 33 { - // Compressed public key length - if i_byte + 33 < script_bytes.len() { - let pk_bytes = &script_bytes[i_byte + 1..i_byte + 34]; - if let Ok(pk) = PublicKey::from_slice(pk_bytes) { - pubkeys.push(pk); - } - i_byte += 34; - } else { - break; - } - } else { - i_byte += 1; - } - } - - if pubkeys.len() != 2 { - return Err(anyhow::anyhow!( - "Input {} witness script does not contain exactly 2 public keys", - i - )); - } - - // Order signatures according to the public keys in the redeem script - // - // TODO(ivanlele): I've spend too much time debugging coin flips here, need to refactor this properly later - let sig1 = input - .partial_sigs - .get(&pubkeys[0]) - .ok_or_else(|| anyhow::anyhow!("Missing signature for first public key"))?; - let sig2 = input - .partial_sigs - .get(&pubkeys[1]) - .ok_or_else(|| anyhow::anyhow!("Missing signature for second public key"))?; - - tx.input[i].witness.script_witness = vec![ - vec![], // OP_0 for multisig - sig1.clone(), - sig2.clone(), - witness_script.to_bytes(), - ]; - } - - // Verify all inputs have witnesses - let all_finalized = tx - .input - .iter() - .all(|input| !input.witness.script_witness.is_empty()); - - if !all_finalized { - return Err(anyhow::anyhow!( - "Not all inputs are finalized - transaction may be missing signatures" - )); - } - - let witnesses: Vec<_> = tx - .input - .iter() - .enumerate() - .map(|(idx, input)| { - json!({ - "input_index": idx, - "witness_elements": input.witness.script_witness.len() - }) - }) - .collect(); - - let output = json!({ - "transaction_hex": hex::encode(serialize(&tx)), - "txid": tx.txid().to_string(), - "inputs": tx.input.len(), - "outputs": tx.output.len(), - "finalized": true, - "witnesses": witnesses - }); - - println!("{}", serde_json::to_string_pretty(&output)?); - - Ok(()) -} diff --git a/cli/src/commands/tx/mod.rs b/cli/src/commands/tx/mod.rs deleted file mode 100644 index 4ef0875..0000000 --- a/cli/src/commands/tx/mod.rs +++ /dev/null @@ -1,3 +0,0 @@ -pub mod create; -pub mod finalize; -pub mod sign; diff --git a/cli/src/commands/tx/sign.rs b/cli/src/commands/tx/sign.rs deleted file mode 100644 index 90d2e24..0000000 --- a/cli/src/commands/tx/sign.rs +++ /dev/null @@ -1,97 +0,0 @@ -use anyhow::{Context, Result}; -use hal_simplicity::simplicity::elements::{ - EcdsaSighashType, - bitcoin::PublicKey, - encode::{deserialize, serialize}, - hashes::Hash, - pset::PartiallySignedTransaction, - script::Script, - secp256k1_zkp::{Message, Secp256k1, SecretKey}, - sighash::SighashCache, -}; -use serde_json::json; - -pub fn execute( - pset_hex: &str, - secret_key_hex: &str, - input_index: usize, - redeem_script_hex: &str, -) -> Result<()> { - let pset_bytes = hex::decode(pset_hex).context("Failed to decode PSET hex")?; - let mut pset: PartiallySignedTransaction = - deserialize(&pset_bytes).context("Failed to deserialize PSET")?; - - if input_index >= pset.inputs().len() { - return Err(anyhow::anyhow!( - "Input index {} out of bounds (PSET has {} inputs)", - input_index, - pset.inputs().len() - )); - } - - let secret_key_bytes = - hex::decode(secret_key_hex).context("Failed to decode secret key hex")?; - let secret_key = SecretKey::from_slice(&secret_key_bytes).context("Invalid secret key")?; - - let redeem_script_bytes = - hex::decode(redeem_script_hex).context("Failed to decode redeem script hex")?; - let redeem_script = Script::from(redeem_script_bytes); - - let secp = Secp256k1::new(); - - let public_key = PublicKey::from_private_key( - &secp, - &hal_simplicity::simplicity::elements::bitcoin::PrivateKey { - compressed: true, - network: hal_simplicity::simplicity::elements::bitcoin::NetworkKind::Main, - inner: secret_key, - }, - ); - - let tx = pset.extract_tx()?; - - let pset_input = &pset.inputs()[input_index]; - - let prev_value = pset_input - .witness_utxo - .as_ref() - .ok_or_else(|| anyhow::anyhow!("Missing witness UTXO for input {}", input_index))? - .value; - - // Compute sighash for P2WSH (SegWit v0) - let mut sighash_cache = SighashCache::new(&tx); - let sighash = sighash_cache.segwitv0_sighash( - input_index, - &redeem_script, - prev_value, - EcdsaSighashType::All, - ); - - let msg = Message::from_digest(sighash.to_byte_array()); - let signature = secp.sign_ecdsa(&msg, &secret_key); - - let mut sig_bytes = signature.serialize_der().to_vec(); - sig_bytes.push(EcdsaSighashType::All.as_u32() as u8); - - // Add signature to PSET - let input = &mut pset.inputs_mut()[input_index]; - input.partial_sigs.insert(public_key, sig_bytes.clone()); - - if input.witness_script.is_none() { - input.witness_script = Some(redeem_script.clone()); - } - - let partial_sigs_count = pset.inputs()[input_index].partial_sigs.len(); - - let output = json!({ - "pset": hex::encode(serialize(&pset)), - "signature_hex": hex::encode(&sig_bytes), - "public_key_hex": hex::encode(public_key.to_bytes()), - "input_index": input_index, - "partial_sigs_count": partial_sigs_count, - }); - - println!("{}", serde_json::to_string_pretty(&output)?); - - Ok(()) -} diff --git a/cli/src/main.rs b/cli/src/main.rs index ac654a2..739fb2a 100644 --- a/cli/src/main.rs +++ b/cli/src/main.rs @@ -5,7 +5,7 @@ use clap::{Parser, Subcommand}; #[derive(Parser)] #[command(name = "simplicity-unchained")] -#[command(about = "A CLI tool for Simplicity Unchained utilities", long_about = None)] +#[command(about = "Profile-bound policy signing utilities for Simplicity Unchained")] struct Cli { #[command(subcommand)] command: Commands, @@ -13,47 +13,21 @@ struct Cli { #[derive(Subcommand)] enum Commands { - /// Address operations - Address { + /// Confidential multi-asset liquid-policy-v1 utilities (not legacy tx) + LiquidPolicy { #[command(subcommand)] - command: AddressCommands, + command: Box, + }, + /// Bitcoin policy-profile funding, evaluation and signing + Policy { + #[command(subcommand)] + command: Box, }, - /// Keypair operations Keypair { #[command(subcommand)] command: KeypairCommands, }, - - /// Transaction operations (Elements/Liquid) - Tx { - #[command(subcommand)] - command: TxCommands, - }, - - /// Bitcoin transaction operations - BtcTx { - #[command(subcommand)] - command: BtcTxCommands, - }, -} - -#[derive(Subcommand)] -enum AddressCommands { - /// Generate a 2-of-2 multisig address from two public keys - Multisig { - /// First public key in hex format - #[arg(short = '1', long)] - pubkey1: String, - - /// Second public key in hex format - #[arg(short = '2', long)] - pubkey2: String, - - /// Network (elements, liquid, liquid_testnet, bitcoin, testnet, testnet4) - #[arg(short, long, default_value = "elements")] - network: String, - }, } #[derive(Subcommand)] @@ -62,165 +36,12 @@ enum KeypairCommands { Generate, } -#[derive(Subcommand)] -enum BtcTxCommands { - /// Create a PSBT (PartiallySignedTransaction) from UTXOs - Create { - /// Transaction input in format: txid:vout - #[arg(short = 'i', long = "input", num_args = 1.., required = true)] - inputs: Vec, - - /// Transaction output in format: address:value - #[arg(short = 'o', long = "output", num_args = 1.., required = true)] - outputs: Vec, - - /// Network (bitcoin, testnet) - #[arg(short, long, default_value = "bitcoin")] - network: String, - }, - - /// Sign a PSBT with one secret key (for co-signing) - Sign { - /// PSBT in hex format - #[arg(short, long)] - psbt: String, - - /// Secret key in hex format - #[arg(short = 'k', long)] - secret_key: String, - - /// Input index to sign - #[arg(short, long)] - input_index: usize, - - /// Redeem script in hex format - #[arg(short, long)] - redeem_script: String, - }, - - /// Finalize a PSBT into a broadcastable transaction - Finalize { - /// PSBT in hex format - #[arg(short, long)] - psbt: String, - }, -} - -#[derive(Subcommand)] -enum TxCommands { - /// Create a PSET (PartiallySignedTransaction) from UTXOs - Create { - /// Transaction input in format: txid:vout - #[arg(short = 'i', long = "input", num_args = 1.., required = true)] - inputs: Vec, - - /// Transaction output in format: address:value - #[arg(short = 'o', long = "output", num_args = 1.., required = true)] - outputs: Vec, - - /// Asset ID (defaults to L-BTC on Liquid/Elements) - #[arg(short, long)] - asset: Option, - - /// Network (elements, liquid, liquid_testnet) - #[arg(short, long, default_value = "elements")] - network: String, - }, - - /// Sign a PSET with one secret key (for co-signing) - Sign { - /// PSET in hex format - #[arg(short, long)] - pset: String, - - /// Secret key in hex format - #[arg(short = 'k', long)] - secret_key: String, - - /// Input index to sign - #[arg(short, long)] - input_index: usize, - - /// Redeem script in hex format - #[arg(short, long)] - redeem_script: String, - }, - - /// Finalize a PSET into a broadcastable transaction - Finalize { - /// PSET in hex format - #[arg(short, long)] - pset: String, - }, -} - fn main() -> Result<()> { - let cli = Cli::parse(); - - match cli.command { - Commands::Address { command } => match command { - AddressCommands::Multisig { - pubkey1, - pubkey2, - network, - } => { - commands::address::multisig::execute(&pubkey1, &pubkey2, &network)?; - } - }, - - Commands::Keypair { command } => match command { - KeypairCommands::Generate => { - commands::keypair::generate::execute()?; - } - }, - - Commands::Tx { command } => match command { - TxCommands::Create { - inputs, - outputs, - asset, - network, - } => { - commands::tx::create::execute(&inputs, &outputs, asset.as_deref(), &network)?; - } - - TxCommands::Sign { - pset, - secret_key, - input_index, - redeem_script, - } => { - commands::tx::sign::execute(&pset, &secret_key, input_index, &redeem_script)?; - } - - TxCommands::Finalize { pset } => { - commands::tx::finalize::execute(&pset)?; - } - }, - - Commands::BtcTx { command } => match command { - BtcTxCommands::Create { - inputs, - outputs, - network, - } => { - commands::btc_tx::create::execute(&inputs, &outputs, &network)?; - } - - BtcTxCommands::Sign { - psbt, - secret_key, - input_index, - redeem_script, - } => { - commands::btc_tx::sign::execute(&psbt, &secret_key, input_index, &redeem_script)?; - } - - BtcTxCommands::Finalize { psbt } => { - commands::btc_tx::finalize::execute(&psbt)?; - } - }, + match Cli::parse().command { + Commands::LiquidPolicy { command } => commands::liquid_policy::execute(*command), + Commands::Policy { command } => commands::policy::execute(*command), + Commands::Keypair { + command: KeypairCommands::Generate, + } => commands::keypair::generate::execute(), } - - Ok(()) } diff --git a/cli/tests/common/mod.rs b/cli/tests/common/mod.rs new file mode 100644 index 0000000..630ae13 --- /dev/null +++ b/cli/tests/common/mod.rs @@ -0,0 +1,35 @@ +use std::{ + fs::{self, OpenOptions}, + io::Write, + path::PathBuf, + sync::atomic::{AtomicUsize, Ordering}, +}; + +pub struct SecretFile(pub PathBuf); + +impl SecretFile { + pub fn new(secret: [u8; 32]) -> Self { + static NEXT: AtomicUsize = AtomicUsize::new(0); + let path = std::env::current_dir().unwrap().join(format!( + ".policy-test-key-{}-{}", + std::process::id(), + NEXT.fetch_add(1, Ordering::Relaxed), + )); + let mut options = OpenOptions::new(); + options.write(true).create_new(true); + #[cfg(unix)] + { + use std::os::unix::fs::OpenOptionsExt; + options.mode(0o600); + } + let mut file = options.open(&path).unwrap(); + file.write_all(hex::encode(secret).as_bytes()).unwrap(); + Self(path) + } +} + +impl Drop for SecretFile { + fn drop(&mut self) { + fs::remove_file(&self.0).unwrap(); + } +} diff --git a/cli/tests/liquid_policy.rs b/cli/tests/liquid_policy.rs new file mode 100644 index 0000000..8ea4289 --- /dev/null +++ b/cli/tests/liquid_policy.rs @@ -0,0 +1,207 @@ +#[allow(dead_code)] +mod common { + include!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../policy/tests/common/liquid.rs" + )); +} +use common::*; +use serde_json::{Value, json}; +use simplicity::base64::{Engine, engine::general_purpose::STANDARD}; +use simplicity_unchained_policy::liquid::{self as policy, elements::encode::serialize}; +use std::{path::PathBuf, process::Command}; + +struct File(PathBuf); +impl File { + fn new(label: &str, data: &[u8]) -> Self { + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(format!( + "target-liquid-test-{}-{label}.json", + std::process::id() + )); + std::fs::write(&path, data).unwrap(); + Self(path) + } +} +impl Drop for File { + fn drop(&mut self) { + std::fs::remove_file(&self.0).unwrap(); + } +} +fn request(f: &Fixture) -> Value { + json!({"profile":"liquid-policy-v1","network":"liquidtestnet","program":STANDARD.encode(&f.program),"redeem_script_hex":hex::encode(&f.data),"base_public_key_hex":f.base.to_string(),"pset_hex":hex::encode(serialize(&f.pset)),"input_index":0,"user_pubkey":f.user.to_string(),"user_leaf_hash_hex":hex::encode(f.recovery.to_byte_array()),"witness":""}) +} +fn run(op: &str, file: &File) -> std::process::Output { + Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(["liquid-policy", op, "--request-file"]) + .arg(&file.0) + .output() + .unwrap() +} +#[test] +fn liquid_cli_registry_prepare_evaluate_address_finalize_and_rejections() { + let mut f = Fixture::new(); + let registry = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(["liquid-policy", "registry"]) + .output() + .unwrap(); + assert!(registry.status.success()); + let registry: Value = serde_json::from_slice(®istry.stdout).unwrap(); + assert_eq!( + registry["profile_hash"], + hex::encode(policy::profile_hash()) + ); + let file = File::new("unsigned", &serde_json::to_vec(&request(&f)).unwrap()); + for operation in ["prepare", "approval-hash", "evaluate", "tweak", "address"] { + let output = run(operation, &file); + assert!( + output.status.success(), + "{operation}: {}", + String::from_utf8_lossy(&output.stderr) + ); + let response: Value = serde_json::from_slice(&output.stdout).unwrap(); + if let Some(context) = response.get("context") { + assert_eq!( + context, + &serde_json::to_value(f.context(0).unwrap().summary()).unwrap() + ); + } + } + f.sign_all(); + let signed = File::new("signed", &serde_json::to_vec(&request(&f)).unwrap()); + let key = File::new( + "user-key", + hex::encode(f.user_secret.secret_bytes()).as_bytes(), + ); + let cosigned = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(["liquid-policy", "user-sign", "--request-file"]) + .arg(&signed.0) + .arg("--secret-key-file") + .arg(&key.0) + .output() + .unwrap(); + assert!( + cosigned.status.success(), + "{}", + String::from_utf8_lossy(&cosigned.stderr) + ); + let cosigned: Value = serde_json::from_slice(&cosigned.stdout).unwrap(); + assert_eq!(cosigned["public_key_hex"], f.user.to_string()); + let secp = policy::elements::secp256k1_zkp::Secp256k1::new(); + let blinding = + policy::elements::secp256k1_zkp::PublicKey::from_secret_key(&secp, &f.blinding_secret); + let creation = json!({ + "network":"liquidtestnet", + "inputs":f.pset.inputs().iter().map(|i|json!({"txid":i.previous_txid.to_string(),"vout":i.previous_output_index,"parent_tx_hex":hex::encode(serialize(i.non_witness_utxo.as_ref().unwrap())),"blinding_secret_key_hex":hex::encode(f.blinding_secret.secret_bytes())})).collect::>(), + "outputs":f.pset.outputs().iter().map(|o|json!({"address":if o.script_pubkey.is_empty() {"fee".into()} else {policy::elements::Address::from_script(&o.script_pubkey,Some(blinding),f.network.address_params()).unwrap().to_string()},"asset_id":o.asset.unwrap().to_string(),"value_sat":o.amount.unwrap()})).collect::>(), + }); + let create_file = File::new("create", &serde_json::to_vec(&creation).unwrap()); + let created = run("create", &create_file); + assert!( + created.status.success(), + "{}", + String::from_utf8_lossy(&created.stderr) + ); + let created: Value = serde_json::from_slice(&created.stdout).unwrap(); + let p = policy::elements::encode::deserialize( + &hex::decode(created["pset_hex"].as_str().unwrap()).unwrap(), + ) + .unwrap(); + assert!(f.context_for(&p, 0).is_ok()); + let output = run("finalize", &signed); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); + assert_eq!( + serde_json::from_slice::(&output.stdout).unwrap()["finalized"], + true + ); + let mut wrong = request(&f); + wrong["profile_hash"] = json!(hex::encode(simplicity_unchained_policy::profile_hash())); + let bad = File::new("profile", &serde_json::to_vec(&wrong).unwrap()); + assert!(!run("prepare", &bad).status.success()); + let mut p = f.pset.clone(); + p.inputs_mut()[0].amount = Some(1); + let mut wrong = request(&f); + wrong["pset_hex"] = hex::encode(serialize(&p)).into(); + let bad = File::new("disclosure", &serde_json::to_vec(&wrong).unwrap()); + assert!(!run("prepare", &bad).status.success()); +} + +#[test] +fn liquid_cli_remote_sign_independently_checks_complete_context_and_signature() { + use std::{ + io::{Read, Write}, + net::TcpListener, + time::Duration, + }; + let mut f = Fixture::new(); + let unsigned = request(&f); + let prepared = policy::PreparedPolicy::new( + &f.program, &f.pset, 0, &f.data, f.network, f.base, f.user, f.recovery, + ) + .unwrap(); + let context = prepared.context().clone(); + let cost = prepared.evaluate_default(&[]).unwrap(); + let secret = f.service_secret(); + let sig = policy::add_signature(&context, &mut f.pset, secret, true).unwrap(); + let capabilities = json!({"profile":"liquid-policy-v1","profile_hash":hex::encode(policy::profile_hash()),"network":"liquidtestnet","genesis_hash":hex::encode(f.network.genesis_hash().to_byte_array()),"base_public_key_hex":f.base.to_string(),"manifest":policy::manifest()}); + let signed = json!({"pset_hex":hex::encode(serialize(&f.pset)),"signature_hex":hex::encode(sig.sig.as_ref()),"public_key_hex":context.cooperative().service_pubkey.to_string(),"input_index":0,"context":context.summary(),"execution_cost_milliweight":cost}); + for tamper in [false, true] { + let mut response = signed.clone(); + if tamper { + response["signature_hex"] = json!(hex::encode([1; 64])); + } + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let port = listener.local_addr().unwrap().port(); + let caps = capabilities.clone(); + let server = std::thread::spawn(move || { + for response in [caps, response] { + let (mut socket, _) = listener.accept().unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(10))) + .unwrap(); + let mut bytes = Vec::new(); + let mut buffer = [0; 4096]; + loop { + let read = socket.read(&mut buffer).unwrap(); + assert!(read > 0); + bytes.extend_from_slice(&buffer[..read]); + if let Some(header) = bytes.windows(4).position(|w| w == b"\r\n\r\n") { + let length = String::from_utf8_lossy(&bytes[..header]) + .lines() + .find_map(|line| { + line.to_ascii_lowercase() + .strip_prefix("content-length: ") + .map(|s| s.parse::().unwrap()) + }) + .unwrap_or(0); + if bytes.len() >= header + 4 + length { + break; + } + } + } + let body = response.to_string(); + write!(socket,"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{}",body.len(),body).unwrap(); + } + }); + let file = File::new( + if tamper { "remote-bad" } else { "remote" }, + &serde_json::to_vec(&unsigned).unwrap(), + ); + let result = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(["liquid-policy", "sign", "--request-file"]) + .arg(&file.0) + .args(["--service", &format!("http://127.0.0.1:{port}")]) + .output() + .unwrap(); + server.join().unwrap(); + assert_eq!( + result.status.success(), + !tamper, + "{}", + String::from_utf8_lossy(&result.stderr) + ); + } +} diff --git a/cli/tests/policy.rs b/cli/tests/policy.rs new file mode 100644 index 0000000..35e7b1c --- /dev/null +++ b/cli/tests/policy.rs @@ -0,0 +1,1081 @@ +mod common; + +use std::{ + io::{Read, Write}, + net::{TcpListener, TcpStream}, + process::{Command, Output}, + sync::Arc, + thread::{self, JoinHandle}, + time::{Duration, Instant}, +}; + +use serde_json::{Value, json}; +use simplicity::{ + ConstructNode, + base64::prelude::{BASE64_STANDARD, Engine as _}, + bitcoin::{ + self as btc, Amount, Network, OutPoint, Psbt, PublicKey, ScriptBuf, TapNodeHash, + Transaction, TxIn, TxOut, XOnlyPublicKey, + consensus::serialize, + hashes::Hash, + key::TapTweak, + secp256k1::{Keypair, Message, Parity, Secp256k1, SecretKey}, + taproot::LeafVersion, + }, + node::CoreConstructible, + types, +}; +use simplicity_unchained_core::{ + policy::{PreparedPolicy, policy_cmr}, + utils::generate_p2tr_address_btc, +}; +use simplicity_unchained_policy::{ + PROFILE_NAME, genesis_hash, manifest, profile_hash, signing_commitment, tweak_public_key, +}; + +const PREFIX: &str = "/simplicity-unchained/policy/bitcoin"; + +struct Fixture { + program: Vec, + script: Vec, + psbt: Psbt, + base: Keypair, + user: Keypair, + recovery: TapNodeHash, +} + +impl Fixture { + fn new() -> Self { + let program = types::Context::with_context(|ctx| { + Arc::::unit(&ctx) + .finalize_types() + .unwrap() + .to_vec_without_witness() + }); + let script = vec![0x52]; + let secp = Secp256k1::new(); + let key = + |byte| Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[byte; 32]).unwrap()); + let base = key(1); + let user = key(2); + let recovery = TapNodeHash::from_byte_array([3; 32]); + let cmr = policy_cmr(&program).unwrap(); + let service = tweak_public_key( + base.x_only_public_key().0, + cmr.to_byte_array(), + &script, + Network::Testnet4, + ) + .unwrap(); + let full = |key: XOnlyPublicKey| PublicKey::new(key.public_key(Parity::Even)); + let (address, _) = generate_p2tr_address_btc( + &full(service), + &full(user.x_only_public_key().0), + recovery, + Network::Testnet4, + ) + .unwrap(); + let parent = Transaction { + version: btc::transaction::Version::TWO, + lock_time: btc::absolute::LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![ + TxOut { + value: Amount::from_sat(100_000), + script_pubkey: address.script_pubkey(), + }; + 2 + ], + }; + let tx = Transaction { + version: btc::transaction::Version::TWO, + lock_time: btc::absolute::LockTime::ZERO, + input: (0..2) + .map(|vout| TxIn { + previous_output: OutPoint::new(parent.compute_txid(), vout), + ..TxIn::default() + }) + .collect(), + output: vec![TxOut { + value: Amount::from_sat(199_000), + script_pubkey: ScriptBuf::new(), + }], + }; + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + for (index, input) in psbt.inputs.iter_mut().enumerate() { + input.non_witness_utxo = Some(parent.clone()); + input.witness_utxo = Some(parent.output[index].clone()); + } + Self { + program, + script, + psbt, + base, + user, + recovery, + } + } + + fn prepared(&self) -> PreparedPolicy { + PreparedPolicy::new( + &self.program, + &self.psbt, + 0, + &self.script, + Network::Testnet4, + self.base.x_only_public_key().0, + self.user.x_only_public_key().0, + self.recovery, + ) + .unwrap() + } + + fn capabilities(&self) -> Value { + let mut limits = serde_json::to_value(&manifest().limits).unwrap(); + limits["max_body_bytes"] = json!(2 * 1024 * 1024); + limits["max_blocking_jobs"] = json!(8); + json!({ + "profile": PROFILE_NAME, + "profile_hash": hex::encode(profile_hash()), + "network": "testnet4", + "genesis_hash": hex::encode(genesis_hash(Network::Testnet4).unwrap()), + "base_public_key_hex": self.base.x_only_public_key().0.to_string(), + "manifest": manifest(), + "limits": limits, + "provenance": "parent-txid-verified", + }) + } + + fn args(&self, operation: &str) -> Vec { + [ + "policy".into(), + operation.into(), + "--network".into(), + "testnet4".into(), + "--program".into(), + BASE64_STANDARD.encode(&self.program), + "--redeem-script-hex".into(), + hex::encode(&self.script), + "--psbt".into(), + hex::encode(self.psbt.serialize()), + "--input-index".into(), + "0".into(), + "--user-pubkey".into(), + self.user.public_key().to_string(), + "--user-leaf-hash-hex".into(), + hex::encode(self.recovery.to_byte_array()), + "--base-public-key".into(), + self.base.x_only_public_key().0.to_string(), + ] + .into() + } + + fn sign_response(&self, include_user: bool) -> Value { + let prepared = self.prepared(); + let context = prepared.context(); + let commitment = signing_commitment( + prepared.cmr().to_byte_array(), + &self.script, + Network::Testnet4, + ) + .unwrap(); + let tweaked = self + .base + .tap_tweak( + &Secp256k1::new(), + Some(TapNodeHash::from_byte_array(commitment)), + ) + .to_keypair(); + let message = Message::from_digest(context.cooperative_sighash_default()); + let signature = Secp256k1::new().sign_schnorr(&message, &tweaked); + let leaf_hash = context.cooperative_leaf_hash(); + let mut psbt = self.psbt.clone(); + psbt.inputs[0].tap_script_sigs.insert( + (context.service_pubkey(), leaf_hash), + btc::taproot::Signature { + signature, + sighash_type: btc::TapSighashType::Default, + }, + ); + if include_user { + psbt.inputs[0].tap_script_sigs.insert( + (context.user_pubkey(), leaf_hash), + btc::taproot::Signature { + signature: Secp256k1::new().sign_schnorr(&message, &self.user), + sighash_type: btc::TapSighashType::Default, + }, + ); + } + psbt.inputs[0].tap_scripts.insert( + context.control_block().clone(), + (context.cooperative_script().clone(), LeafVersion::TapScript), + ); + json!({ + "psbt_hex": hex::encode(psbt.serialize()), + "signature_hex": hex::encode(signature.as_ref()), + "public_key_hex": context.service_pubkey().to_string(), + "input_index": 0, + "context": prepared.summary(), + "execution_cost_milliweight": prepared.evaluate_default(&[]).unwrap().cost_milliweight, + }) + } +} + +struct Reply { + method: &'static str, + path: String, + status: u16, + body: Vec, +} + +impl Reply { + fn json(method: &'static str, operation: &str, body: Value) -> Self { + Self { + method, + path: format!("{PREFIX}/{operation}"), + status: 200, + body: serde_json::to_vec(&body).unwrap(), + } + } +} + +struct MockServer { + origin: String, + thread: JoinHandle)>>, +} + +impl MockServer { + fn start(replies: Vec) -> Self { + let listener = TcpListener::bind("127.0.0.1:0").unwrap(); + let origin = format!("http://{}", listener.local_addr().unwrap()); + listener.set_nonblocking(true).unwrap(); + let thread = thread::spawn(move || { + let mut requests = Vec::new(); + for reply in replies { + let deadline = Instant::now() + Duration::from_secs(10); + let (mut stream, _) = loop { + match listener.accept() { + Ok(connected) => break connected, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + assert!( + Instant::now() < deadline, + "Expected HTTP request was not made" + ); + thread::sleep(Duration::from_millis(5)); + } + Err(error) => panic!("Mock accept failed: {error}"), + } + }; + stream.set_nonblocking(false).unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); + let (headers, body) = read_request(&mut stream); + assert!( + headers.starts_with(&format!("{} {} HTTP/1.1", reply.method, reply.path)), + "Unexpected request: {headers}" + ); + write!( + stream, + "HTTP/1.1 {} Test\r\nContent-Length: {}\r\nConnection: close\r\nContent-Type: application/json\r\n\r\n", + reply.status, + reply.body.len() + ) + .unwrap(); + stream.write_all(&reply.body).unwrap(); + requests.push((headers, body)); + } + requests + }); + Self { origin, thread } + } + + fn finish(self) -> Vec<(String, Vec)> { + self.thread.join().unwrap() + } +} + +fn read_request(stream: &mut TcpStream) -> (String, Vec) { + let mut bytes = Vec::new(); + let header_end = loop { + let mut chunk = [0; 4096]; + let count = stream.read(&mut chunk).unwrap(); + assert_ne!(count, 0, "Request ended before headers"); + bytes.extend_from_slice(&chunk[..count]); + if let Some(position) = bytes.windows(4).position(|part| part == b"\r\n\r\n") { + break position + 4; + } + }; + let headers = String::from_utf8(bytes[..header_end].to_vec()).unwrap(); + let content_length = headers + .lines() + .find_map(|line| { + let (name, value) = line.split_once(':')?; + name.eq_ignore_ascii_case("content-length") + .then(|| value.trim().parse::().unwrap()) + }) + .unwrap_or(0); + while bytes.len() < header_end + content_length { + let mut chunk = [0; 4096]; + let count = stream.read(&mut chunk).unwrap(); + assert_ne!(count, 0, "Request body ended early"); + bytes.extend_from_slice(&chunk[..count]); + } + (headers, bytes[header_end..].to_vec()) +} + +fn invoke(args: &[String]) -> Output { + Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(args) + .output() + .unwrap() +} + +fn success(output: Output) -> Value { + assert!( + output.status.success(), + "stdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap() +} + +fn failure(output: Output, expected: &str) -> Value { + assert!(!output.status.success(), "Command unexpectedly succeeded"); + let value: Value = serde_json::from_slice(&output.stdout).unwrap(); + let error = value["error"].as_str().expect("Stable JSON error output"); + assert!(error.contains(expected), "Expected {expected}, got {error}"); + assert!(value.get("signature_hex").is_none()); + value +} + +fn set_arg(args: &mut [String], name: &str, value: &str) { + let index = args.iter().position(|arg| arg == name).unwrap(); + args[index + 1] = value.to_owned(); +} + +fn remote_args(fixture: &Fixture, operation: &str, origin: &str) -> Vec { + let mut args = fixture.args(operation); + args.extend(["--service".into(), origin.into()]); + if matches!(operation, "evaluate" | "sign") { + args.extend(["--witness".into(), "".into()]); + } + args +} + +#[test] +fn policy_help_exposes_all_primary_operations() { + let result = invoke(&["policy".into(), "--help".into()]); + assert!(result.status.success()); + let help = String::from_utf8(result.stdout).unwrap(); + for name in [ + "capabilities", + "tweak", + "prepare", + "evaluate", + "approval-hash", + "sign", + "address", + "build-csv-leaf", + "user-sign", + "finalize", + ] { + assert!(help.contains(name)); + } +} + +#[test] +fn local_approval_uses_the_shared_verified_constructor_without_contacting_service() { + let fixture = Fixture::new(); + let expected = serde_json::to_value(fixture.prepared().summary()).unwrap(); + let mut args = fixture.args("approval-hash"); + args.push("--offline".into()); + let actual = success(invoke(&args)); + assert_eq!(actual, json!({ "context": expected })); + assert_eq!(actual["context"]["provenance"], "parent-txid-verified"); + assert_eq!(actual["context"]["chain_status_verified"], false); + set_arg(&mut args, "--input-index", "1"); + let another_input = success(invoke(&args)); + assert_eq!( + actual["context"]["policy_transaction_approval_hash"], + another_input["context"]["policy_transaction_approval_hash"] + ); + assert_ne!( + actual["context"]["policy_approval_hash"], + another_input["context"]["policy_approval_hash"] + ); + set_arg(&mut args, "--input-index", "0"); + let mut mutated = fixture.psbt.clone(); + mutated.unsigned_tx.output[0].value = Amount::from_sat(198_999); + set_arg(&mut args, "--psbt", &hex::encode(mutated.serialize())); + let changed = success(invoke(&args)); + assert_ne!( + actual["context"]["policy_approval_hash"], + changed["context"]["policy_approval_hash"] + ); + assert_ne!( + actual["context"]["policy_transaction_approval_hash"], + changed["context"]["policy_transaction_approval_hash"] + ); +} + +#[test] +fn profile_fingerprint_network_and_base64_fail_closed_before_network_io() { + let fixture = Fixture::new(); + for (flag, value, expected) in [ + ("--network", "regtest", "Unsupported policy network"), + ("--network", "Testnet4", "Unsupported policy network"), + ("--program", "not-base64!", "standard-base64"), + ("--base-public-key", "00", "Public key must be"), + ("--user-leaf-hash-hex", "00", "exactly 32 bytes"), + ] { + let mut args = fixture.args("approval-hash"); + set_arg(&mut args, flag, value); + failure(invoke(&args), expected); + } + for (flag, value, expected) in [ + ("--profile", "legacy", "Unsupported policy profile"), + ( + "--profile-hash", + "0000000000000000000000000000000000000000000000000000000000000000", + "compiled registry", + ), + ] { + let mut args = fixture.args("approval-hash"); + args.extend([flag.into(), value.into()]); + failure(invoke(&args), expected); + } +} + +#[test] +fn every_input_requires_full_parent_evidence_even_when_witness_utxo_is_present() { + let mut fixture = Fixture::new(); + fixture.psbt.inputs[1].non_witness_utxo = None; + // Both inputs share a parent. An available full parent can be reused safely. + success(invoke(&fixture.args("approval-hash"))); + fixture.psbt.inputs[1].non_witness_utxo = fixture.psbt.inputs[0].non_witness_utxo.take(); + success(invoke(&fixture.args("approval-hash"))); + for input in &mut fixture.psbt.inputs { + input.non_witness_utxo = None; + } + let mut args = fixture.args("prepare"); + args.extend([ + "--offline".into(), + "--service".into(), + "http://127.0.0.1:1".into(), + ]); + failure( + invoke(&args), + "missing non_witness_utxo full parent evidence", + ); +} + +#[test] +fn parent_content_is_verified_and_missing_witness_utxo_is_populated() { + let mut fixture = Fixture::new(); + let expected = fixture.prepared().summary(); + fixture.psbt.inputs[0].witness_utxo = None; + let result = success(invoke(&fixture.args("approval-hash"))); + assert_eq!(result["context"], serde_json::to_value(expected).unwrap()); + + fixture.psbt.inputs[0].witness_utxo = Some(TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::new(), + }); + failure( + invoke(&fixture.args("approval-hash")), + "conflicts with verified parent", + ); + fixture.psbt.inputs[0].witness_utxo = None; + fixture.psbt.inputs[0] + .non_witness_utxo + .as_mut() + .unwrap() + .output[0] + .value = Amount::from_sat(1); + failure( + invoke(&fixture.args("approval-hash")), + "parent transaction txid mismatch", + ); +} + +#[test] +fn capabilities_checks_full_manifest_network_profile_and_genesis() { + let fixture = Fixture::new(); + let expected = fixture.capabilities(); + let server = MockServer::start(vec![Reply::json("GET", "capabilities", expected.clone())]); + let args = [ + "policy".into(), + "capabilities".into(), + "--network".into(), + "testnet4".into(), + "--service".into(), + server.origin.clone(), + ]; + assert_eq!(success(invoke(&args)), expected); + server.finish(); + + for (field, replacement, expected_error) in [ + ("profile", json!("legacy"), "profile mismatch"), + ("profile_hash", json!("00"), "fingerprint mismatch"), + ("network", json!("testnet"), "network mismatch"), + ("genesis_hash", json!("00"), "genesis hash mismatch"), + ( + "provenance", + json!("confirmed"), + "verification provenance mismatch", + ), + ] { + let mut caps = fixture.capabilities(); + caps[field] = replacement; + let server = MockServer::start(vec![Reply::json("GET", "capabilities", caps)]); + let mut args = args.clone(); + set_arg(&mut args, "--service", &server.origin); + failure(invoke(&args), expected_error); + server.finish(); + } + for (field, child, value, expected_error) in [ + ("manifest", "status", json!("wrong"), "manifest differs"), + ( + "manifest", + "unknown_semantic_field", + json!(true), + "manifest differs", + ), + ("limits", "max_inputs", json!(9), "limits differ"), + ] { + let mut caps = fixture.capabilities(); + caps[field][child] = value; + let server = MockServer::start(vec![Reply::json("GET", "capabilities", caps)]); + let mut args = args.clone(); + set_arg(&mut args, "--service", &server.origin); + failure(invoke(&args), expected_error); + server.finish(); + } +} + +#[test] +fn tweak_is_profile_bound_and_validated_locally_before_address_generation() { + let fixture = Fixture::new(); + let prepared = fixture.prepared(); + let context = prepared.context(); + let expected = json!({ + "profile": PROFILE_NAME, + "profile_hash": hex::encode(profile_hash()), + "network": "testnet4", + "genesis_hash": hex::encode(genesis_hash(Network::Testnet4).unwrap()), + "cmr": hex::encode(context.cmr()), + "signing_commitment": hex::encode(context.signing_commitment()), + "public_key_hex": context.service_pubkey().to_string(), + "base_public_key_hex": fixture.base.x_only_public_key().0.to_string(), + }); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "tweak", expected.clone()), + ]); + let args = [ + "policy".into(), + "tweak".into(), + "--network".into(), + "testnet4".into(), + "--service".into(), + server.origin.clone(), + "--program".into(), + BASE64_STANDARD.encode(&fixture.program), + "--redeem-script-hex".into(), + hex::encode(&fixture.script), + "--base-public-key".into(), + fixture.base.x_only_public_key().0.to_string(), + ]; + assert_eq!(success(invoke(&args)), expected); + let requests = server.finish(); + let body: Value = serde_json::from_slice(&requests[1].1).unwrap(); + assert_eq!( + body, + json!({ + "profile_hash": hex::encode(profile_hash()), + "network": "testnet4", + "program": BASE64_STANDARD.encode(&fixture.program), + "redeem_script_hex": hex::encode(&fixture.script), + }) + ); +} + +#[test] +fn prepare_matches_local_digests_and_uses_the_new_http_field_names() { + let fixture = Fixture::new(); + let expected = json!({ "context": fixture.prepared().summary() }); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "prepare", expected.clone()), + ]); + assert_eq!( + success(invoke(&remote_args(&fixture, "prepare", &server.origin))), + expected + ); + let requests = server.finish(); + let body: Value = serde_json::from_slice(&requests[1].1).unwrap(); + assert_eq!(body["psbt_hex"], hex::encode(fixture.psbt.serialize())); + assert_eq!(body["input_index"], 0); + assert_eq!(body["program"], BASE64_STANDARD.encode(&fixture.program)); + assert_eq!(body["redeem_script_hex"], "52"); + assert_eq!(body["user_leaf_hash_hex"], hex::encode([3; 32])); + assert_eq!(body["user_pubkey"], fixture.user.public_key().to_string()); + assert!(body.get("witness").is_none()); + assert!(body.get("psbt").is_none()); + assert!(body.get("user_leaf_hash").is_none()); +} + +#[test] +fn remote_digest_or_signer_key_mismatches_are_not_accepted() { + let fixture = Fixture::new(); + let mut context = serde_json::to_value(fixture.prepared().summary()).unwrap(); + context["policy_transaction_approval_hash"] = json!(hex::encode([9; 32])); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "prepare", json!({ "context": context })), + ]); + failure( + invoke(&remote_args(&fixture, "prepare", &server.origin)), + "differ from the locally verified context", + ); + server.finish(); + + let mut caps = fixture.capabilities(); + caps["base_public_key_hex"] = json!(fixture.user.x_only_public_key().0.to_string()); + let server = MockServer::start(vec![Reply::json("GET", "capabilities", caps)]); + failure( + invoke(&remote_args(&fixture, "prepare", &server.origin)), + "base public key does not match", + ); + server.finish(); +} + +#[test] +fn evaluate_executes_a_real_subcommand_and_rejects_success_shaped_denial() { + let fixture = Fixture::new(); + for accepted in [true, false] { + let expected = json!({ + "accepted": accepted, + "context": fixture.prepared().summary(), + "execution_cost_milliweight": fixture.prepared().evaluate_default(&[]).unwrap().cost_milliweight, + }); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "evaluate", expected.clone()), + ]); + let result = invoke(&remote_args(&fixture, "evaluate", &server.origin)); + if accepted { + assert_eq!(success(result), expected); + } else { + failure(result, "did not accept the policy"); + } + let requests = server.finish(); + let body: Value = serde_json::from_slice(&requests[1].1).unwrap(); + assert_eq!(body["witness"], ""); + } +} + +#[test] +fn sign_verifies_the_actual_cooperative_signature_and_psbt_metadata() { + let fixture = Fixture::new(); + let expected = fixture.sign_response(false); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "sign", expected.clone()), + ]); + assert_eq!( + success(invoke(&remote_args(&fixture, "sign", &server.origin))), + expected + ); + server.finish(); + for (field, value, expected_error) in [ + ( + "signature_hex", + json!(hex::encode([0; 64])), + "invalid cooperative signature", + ), + ("input_index", json!(1), "unexpected input index"), + ( + "execution_cost_milliweight", + json!(10_000_001), + "above the policy execution limit", + ), + ( + "execution_cost_milliweight", + json!(expected["execution_cost_milliweight"].as_u64().unwrap() + 1), + "differs from local policy evaluation", + ), + ] { + let mut invalid = fixture.sign_response(false); + invalid[field] = value; + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "sign", invalid), + ]); + failure( + invoke(&remote_args(&fixture, "sign", &server.origin)), + expected_error, + ); + server.finish(); + } + let mut invalid = fixture.sign_response(false); + let mut psbt = + Psbt::deserialize(&hex::decode(invalid["psbt_hex"].as_str().unwrap()).unwrap()).unwrap(); + psbt.unsigned_tx.output[0].value = Amount::from_sat(198_999); + invalid["psbt_hex"] = json!(hex::encode(psbt.serialize())); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "sign", invalid), + ]); + failure( + invoke(&remote_args(&fixture, "sign", &server.origin)), + "Service changed the transaction", + ); + server.finish(); +} + +#[test] +fn service_errors_have_stable_json_and_nonzero_status_without_signing_retries() { + let fixture = Fixture::new(); + for status in [400, 413, 422, 503, 504] { + let mut denied = Reply::json("POST", "sign", json!({ "error": "explicit denial" })); + denied.status = status; + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + denied, + ]); + let error = failure( + invoke(&remote_args(&fixture, "sign", &server.origin)), + &format!("HTTP {status}"), + ); + assert!(error["error"].as_str().unwrap().contains("explicit denial")); + assert_eq!(server.finish().len(), 2); + } +} + +#[test] +fn malformed_success_responses_and_unexpected_redirects_fail_explicitly() { + let fixture = Fixture::new(); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "prepare", json!({ "accepted": true })), + ]); + failure( + invoke(&remote_args(&fixture, "prepare", &server.origin)), + "Invalid policy service response JSON/shape", + ); + server.finish(); + + let mut redirect = Reply::json("GET", "capabilities", json!({})); + redirect.status = 302; + let server = MockServer::start(vec![redirect]); + failure( + invoke(&remote_args(&fixture, "prepare", &server.origin)), + "HTTP 302", + ); + server.finish(); +} + +#[test] +fn policy_service_signature_flows_through_policy_user_sign_and_verified_finalize() { + let mut fixture = Fixture::new(); + fixture.psbt.unsigned_tx.input.truncate(1); + fixture.psbt.inputs.truncate(1); + fixture.psbt.unsigned_tx.output[0].value = Amount::from_sat(99_000); + let service_response = fixture.sign_response(false); + let server = MockServer::start(vec![ + Reply::json("GET", "capabilities", fixture.capabilities()), + Reply::json("POST", "sign", service_response), + ]); + let signed = success(invoke(&remote_args(&fixture, "sign", &server.origin))); + server.finish(); + let key_file = common::SecretFile::new([2; 32]); + let mut args = fixture.args("user-sign"); + set_arg(&mut args, "--psbt", signed["psbt_hex"].as_str().unwrap()); + args.extend([ + "--secret-key-file".into(), + key_file.0.to_str().unwrap().into(), + ]); + let user_signed = success(invoke(&args)); + let mut args = fixture.args("finalize"); + set_arg( + &mut args, + "--psbt", + user_signed["psbt_hex"].as_str().unwrap(), + ); + let finalized = success(invoke(&args)); + assert_eq!(finalized["finalized"], true); + let tx: Transaction = btc::consensus::deserialize( + &hex::decode(finalized["transaction_hex"].as_str().unwrap()).unwrap(), + ) + .unwrap(); + assert_eq!(tx.input[0].witness.len(), 4); + assert_eq!(tx.output, fixture.psbt.unsigned_tx.output); +} + +#[test] +fn policy_funding_address_matches_verified_context_and_defaults_to_testnet4() { + let fixture = Fixture::new(); + let args = vec![ + "policy".into(), + "address".into(), + "--program".into(), + BASE64_STANDARD.encode(&fixture.program), + "--redeem-script-hex".into(), + hex::encode(&fixture.script), + "--base-public-key".into(), + fixture.base.x_only_public_key().0.to_string(), + "--user-pubkey".into(), + fixture.user.x_only_public_key().0.to_string(), + "--user-leaf-hash-hex".into(), + hex::encode(fixture.recovery.to_byte_array()), + ]; + let address = success(invoke(&args)); + assert_eq!(address["network"], "testnet4"); + assert_eq!(address["profile_hash"], hex::encode(profile_hash())); + assert_eq!( + address["script_pubkey_hex"], + hex::encode( + fixture.psbt.inputs[0] + .witness_utxo + .as_ref() + .unwrap() + .script_pubkey + .as_bytes() + ), + ); + let mut other_network = args.clone(); + other_network.extend(["--network".into(), "testnet".into()]); + let different = success(invoke(&other_network)); + assert_ne!(different["script_pubkey_hex"], address["script_pubkey_hex"]); + let mut bad_profile = args; + bad_profile.extend(["--profile".into(), "legacy".into()]); + failure(invoke(&bad_profile), "Unsupported policy profile"); +} + +#[test] +fn policy_csv_leaf_accepts_xonly_keys_and_requires_a_real_delay() { + let fixture = Fixture::new(); + let args = vec![ + "policy".into(), + "build-csv-leaf".into(), + "--user-pubkey".into(), + fixture.user.x_only_public_key().0.to_string(), + "--timelock".into(), + "144".into(), + ]; + let recovery = success(invoke(&args)); + let script = ScriptBuf::from_hex(recovery["script"].as_str().unwrap()).unwrap(); + assert_eq!( + recovery["leaf_hash"], + hex::encode(btc::TapLeafHash::from_script(&script, LeafVersion::TapScript).to_byte_array()), + ); + let mut bad_delay = args; + set_arg(&mut bad_delay, "--timelock", "0"); + failure( + invoke(&bad_delay), + "Recovery timelock must be at least one block", + ); +} + +#[test] +fn user_sign_rejects_wrong_key_and_unverified_or_different_contracts() { + let mut fixture = Fixture::new(); + let wrong_key = common::SecretFile::new([4; 32]); + let mut args = fixture.args("user-sign"); + args.extend([ + "--secret-key-file".into(), + wrong_key.0.to_str().unwrap().into(), + ]); + failure(invoke(&args), "User secret key does not match"); + for input in &mut fixture.psbt.inputs { + input.non_witness_utxo = None; + } + set_arg(&mut args, "--psbt", &hex::encode(fixture.psbt.serialize())); + failure( + invoke(&args), + "missing non_witness_utxo full parent evidence", + ); + + let fixture = Fixture::new(); + let mut args = fixture.args("user-sign"); + args.extend([ + "--secret-key-file".into(), + wrong_key.0.to_str().unwrap().into(), + ]); + set_arg(&mut args, "--redeem-script-hex", "53"); + failure(invoke(&args), "Failed to construct verified policy context"); +} + +#[test] +fn policy_finalize_requires_valid_signatures_and_committed_leaf_metadata() { + let mut fixture = Fixture::new(); + fixture.psbt.unsigned_tx.input.truncate(1); + fixture.psbt.inputs.truncate(1); + fixture.psbt.unsigned_tx.output[0].value = Amount::from_sat(99_000); + let mut args = fixture.args("finalize"); + failure(invoke(&args), "no committed canonical multisig leaf"); + let signed = fixture.sign_response(true); + let signed_psbt = + Psbt::deserialize(&hex::decode(signed["psbt_hex"].as_str().unwrap()).unwrap()).unwrap(); + set_arg(&mut args, "--psbt", signed["psbt_hex"].as_str().unwrap()); + assert_eq!(success(invoke(&args))["finalized"], true); + + let prepared = fixture.prepared(); + let signature_key = ( + prepared.context().user_pubkey(), + prepared.context().cooperative_leaf_hash(), + ); + let mut missing = signed_psbt.clone(); + missing.inputs[0].tap_script_sigs.remove(&signature_key); + set_arg(&mut args, "--psbt", &hex::encode(missing.serialize())); + failure(invoke(&args), "missing tap_script_sig"); + + let mut invalid = signed_psbt.clone(); + invalid.inputs[0] + .tap_script_sigs + .get_mut(&signature_key) + .unwrap() + .signature = Secp256k1::new().sign_schnorr(&Message::from_digest([0; 32]), &fixture.user); + set_arg(&mut args, "--psbt", &hex::encode(invalid.serialize())); + failure(invoke(&args), "invalid tap_script_sig"); + + let mut wrong_leaf = signed_psbt.clone(); + wrong_leaf.inputs[0] + .tap_scripts + .values_mut() + .next() + .unwrap() + .0 = ScriptBuf::new(); + set_arg(&mut args, "--psbt", &hex::encode(wrong_leaf.serialize())); + failure(invoke(&args), "no committed canonical multisig leaf"); + + let mut wrong_sighash = signed_psbt.clone(); + wrong_sighash.inputs[0] + .tap_script_sigs + .get_mut(&signature_key) + .unwrap() + .sighash_type = btc::TapSighashType::All; + set_arg(&mut args, "--psbt", &hex::encode(wrong_sighash.serialize())); + failure(invoke(&args), "Failed to construct verified policy context"); + + let mut missing_parent = signed_psbt; + missing_parent.inputs[0].non_witness_utxo = None; + set_arg( + &mut args, + "--psbt", + &hex::encode(missing_parent.serialize()), + ); + failure( + invoke(&args), + "missing non_witness_utxo full parent evidence", + ); +} + +#[test] +fn policy_user_sign_and_finalize_verify_every_input() { + let fixture = Fixture::new(); + let mut psbt = fixture.psbt.clone(); + let secp = Secp256k1::new(); + let commitment = fixture.prepared().context().signing_commitment(); + let service_key = fixture + .base + .tap_tweak(&secp, Some(TapNodeHash::from_byte_array(commitment))) + .to_keypair(); + for index in 0..psbt.inputs.len() { + let prepared = PreparedPolicy::new( + &fixture.program, + &fixture.psbt, + index, + &fixture.script, + Network::Testnet4, + fixture.base.x_only_public_key().0, + fixture.user.x_only_public_key().0, + fixture.recovery, + ) + .unwrap(); + let context = prepared.context(); + psbt.inputs[index].tap_script_sigs.insert( + (context.service_pubkey(), context.cooperative_leaf_hash()), + btc::taproot::Signature { + signature: secp.sign_schnorr( + &Message::from_digest(context.cooperative_sighash_default()), + &service_key, + ), + sighash_type: btc::TapSighashType::Default, + }, + ); + psbt.inputs[index].tap_scripts.insert( + context.control_block().clone(), + (context.cooperative_script().clone(), LeafVersion::TapScript), + ); + } + let key_file = common::SecretFile::new([2; 32]); + let mut hex_psbt = hex::encode(psbt.serialize()); + for index in 0..2 { + let mut args = fixture.args("user-sign"); + set_arg(&mut args, "--input-index", &index.to_string()); + set_arg(&mut args, "--psbt", &hex_psbt); + args.extend([ + "--secret-key-file".into(), + key_file.0.to_str().unwrap().into(), + ]); + hex_psbt = success(invoke(&args))["psbt_hex"].as_str().unwrap().into(); + if index == 0 { + let mut finalize = fixture.args("finalize"); + set_arg(&mut finalize, "--psbt", &hex_psbt); + failure(invoke(&finalize), "Input 1 missing tap_script_sig"); + } + } + let mut args = fixture.args("finalize"); + set_arg(&mut args, "--psbt", &hex_psbt); + let output = success(invoke(&args)); + assert_eq!(output["inputs"], 2); + let tx: Transaction = btc::consensus::deserialize( + &hex::decode(output["transaction_hex"].as_str().unwrap()).unwrap(), + ) + .unwrap(); + assert!(tx.input.iter().all(|input| input.witness.len() == 4)); + assert_eq!(tx.output, fixture.psbt.unsigned_tx.output); +} + +#[test] +fn configured_esplora_fetches_full_raw_parent_once_and_verifies_before_service_io() { + let mut fixture = Fixture::new(); + let parent = fixture.psbt.inputs[0].non_witness_utxo.clone().unwrap(); + let expected = fixture.prepared().summary(); + for input in &mut fixture.psbt.inputs { + input.non_witness_utxo = None; + input.witness_utxo = None; + } + let explorer = MockServer::start(vec![Reply { + method: "GET", + path: format!("/tx/{}/raw", parent.compute_txid()), + status: 200, + body: serialize(&parent), + }]); + let mut args = fixture.args("approval-hash"); + args.extend(["--esplora-url".into(), explorer.origin.clone()]); + assert_eq!( + success(invoke(&args))["context"], + serde_json::to_value(expected).unwrap() + ); + assert_eq!(explorer.finish().len(), 1); + + let mut wrong_parent = parent; + wrong_parent.output[0].value = Amount::from_sat(1); + let explorer = MockServer::start(vec![Reply { + method: "GET", + path: format!( + "/tx/{}/raw", + fixture.psbt.unsigned_tx.input[0].previous_output.txid + ), + status: 200, + body: serialize(&wrong_parent), + }]); + set_arg(&mut args, "--esplora-url", &explorer.origin); + failure(invoke(&args), "parent transaction txid mismatch"); + explorer.finish(); +} diff --git a/cli/tests/policy_templates.rs b/cli/tests/policy_templates.rs new file mode 100644 index 0000000..737bdc4 --- /dev/null +++ b/cli/tests/policy_templates.rs @@ -0,0 +1,627 @@ +mod common; + +use std::{collections::BTreeMap, process::Command, time::Duration}; + +use serde::Deserialize; +use simplicity::{ + base64::prelude::{BASE64_STANDARD, Engine as _}, + bitcoin::{ + self as btc, Amount, Network, OutPoint, Psbt, ScriptBuf, Sequence, TapNodeHash, + Transaction, TxIn, TxOut, XOnlyPublicKey, + hashes::{Hash, sha256}, + secp256k1::{Keypair, Message, Secp256k1, SecretKey, schnorr::Signature}, + }, +}; +use simplicity_unchained_core::policy::{PolicyError, PreparedPolicy, policy_cmr}; +use simplicity_unchained_policy::{PROFILE_NAME, cooperative_output, profile_hash}; + +#[derive(Deserialize)] +struct Bundle { + profile: String, + profile_hash: String, + templates: BTreeMap, +} + +#[derive(Deserialize)] +struct Template { + source_sha256: String, + program_base64: String, + cmr_hex: String, + profile_hash: String, + duplicate_keys_program_base64: Option, + duplicate_keys_cmr_hex: Option, +} + +fn templates() -> Bundle { + serde_json::from_str(include_str!("../../examples/policy/compiled.json")).unwrap() +} + +fn program(name: &str) -> Vec { + BASE64_STANDARD + .decode(&templates().templates[name].program_base64) + .unwrap() +} + +fn scalar_key(value: u8) -> Keypair { + let mut secret = [0; 32]; + secret[31] = value; + Keypair::from_secret_key(&Secp256k1::new(), &SecretKey::from_slice(&secret).unwrap()) +} + +#[derive(Clone)] +enum Destination { + Return, + Script(ScriptBuf), +} + +fn recipient() -> Destination { + Destination::Script( + ScriptBuf::from_hex("00141111111111111111111111111111111111111111").unwrap(), + ) +} + +struct Fixture { + program: Vec, + psbt: Psbt, + script: Vec, + base: XOnlyPublicKey, +} + +impl Fixture { + fn new(program: Vec, inputs: usize, outputs: &[(u64, Destination)]) -> Self { + Self::with_base( + program, + inputs, + outputs, + scalar_key(41).x_only_public_key().0, + ) + } + + fn with_base( + program: Vec, + inputs: usize, + outputs: &[(u64, Destination)], + base: XOnlyPublicKey, + ) -> Self { + let script = vec![0x52]; + let cmr = policy_cmr(&program).unwrap(); + let output = cooperative_output( + base, + cmr.to_byte_array(), + &script, + Network::Testnet4, + scalar_key(42).x_only_public_key().0, + TapNodeHash::from_byte_array([43; 32]), + ) + .unwrap(); + let parent = Transaction { + version: btc::transaction::Version::TWO, + lock_time: btc::absolute::LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![ + TxOut { + value: Amount::from_sat(100_000), + script_pubkey: output.script_pubkey.clone(), + }; + inputs + ], + }; + let tx = Transaction { + version: btc::transaction::Version::TWO, + lock_time: btc::absolute::LockTime::from_consensus(1000), + input: (0..inputs) + .map(|vout| TxIn { + previous_output: OutPoint::new(parent.compute_txid(), vout as u32), + sequence: Sequence::from_height(16), + ..TxIn::default() + }) + .collect(), + output: outputs + .iter() + .map(|(value, destination)| TxOut { + value: Amount::from_sat(*value), + script_pubkey: match destination { + Destination::Return => output.script_pubkey.clone(), + Destination::Script(script) => script.clone(), + }, + }) + .collect(), + }; + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + for (index, input) in psbt.inputs.iter_mut().enumerate() { + input.non_witness_utxo = Some(parent.clone()); + input.witness_utxo = Some(parent.output[index].clone()); + } + Self { + program, + psbt, + script, + base, + } + } + + fn prepare(&self, index: usize) -> Result { + PreparedPolicy::new( + &self.program, + &self.psbt, + index, + &self.script, + Network::Testnet4, + self.base, + scalar_key(42).x_only_public_key().0, + TapNodeHash::from_byte_array([43; 32]), + ) + } + + fn update_parent(&mut self, parent: Transaction) { + for (txin, input) in self + .psbt + .unsigned_tx + .input + .iter_mut() + .zip(&mut self.psbt.inputs) + { + txin.previous_output.txid = parent.compute_txid(); + input.witness_utxo = Some(parent.output[txin.previous_output.vout as usize].clone()); + input.non_witness_utxo = Some(parent.clone()); + } + } +} + +fn signature(digest: [u8; 32], key: u8) -> Signature { + Secp256k1::new().sign_schnorr_no_aux_rand(&Message::from_digest(digest), &scalar_key(key)) +} + +// Witness encoding is compact sums, not the padded Option frames used by VM jets. +fn signature_witness(signatures: &[Option]) -> Vec { + let mut bits = Vec::new(); + for signature in signatures { + bits.push(signature.is_some()); + if let Some(signature) = signature { + for byte in signature.as_ref() { + bits.extend((0..8).map(|offset| byte & (1 << (7 - offset)) != 0)); + } + } + } + let mut bytes = vec![0; bits.len().div_ceil(8)]; + for (index, bit) in bits.into_iter().enumerate() { + bytes[index / 8] |= u8::from(bit) << (7 - index % 8); + } + bytes +} + +fn approval_witness(prepared: &PreparedPolicy, input_scoped: bool) -> Vec { + let digest = if input_scoped { + prepared.context().policy_approval_hash() + } else { + prepared.context().policy_transaction_approval_hash() + }; + signature_witness(&[Some(signature(digest, 1)), Some(signature(digest, 2)), None]) +} + +fn source_witness(prepared: &PreparedPolicy) -> Vec { + let context = prepared.context(); + let tag = sha256::Hash::hash(b"SimplicityUnchained/PolicySourceOutpoint/v1").to_byte_array(); + let mut signatures = Vec::new(); + for input in &context.tx().input { + let mut preimage = Vec::new(); + preimage.extend_from_slice(&tag); + preimage.extend_from_slice(&tag); + preimage.extend_from_slice(&context.signing_commitment()); + preimage.extend_from_slice(&input.previous_output.txid.to_byte_array()); + preimage.extend_from_slice(&input.previous_output.vout.to_be_bytes()); + let digest = sha256::Hash::hash(&preimage).to_byte_array(); + signatures.push(Some(signature(digest, 1))); + } + signatures.resize(8, None); + signature_witness(&signatures) +} + +fn rejects(prepared: &PreparedPolicy, witness: &[u8]) { + assert!( + matches!( + prepared.evaluate_default(witness), + Err(PolicyError::PolicyRejected(_)) + ), + "Expected policy rejection, got {:?}", + prepared.evaluate_default(witness) + ); +} + +#[test] +fn compiled_templates_pin_the_reviewed_source_cmr_and_registry() { + let bundle = templates(); + assert_eq!(bundle.profile, PROFILE_NAME); + assert_eq!(bundle.profile_hash, hex::encode(profile_hash())); + let sources = [ + ( + "recipient_allowlist.simf", + include_str!("../../examples/policy/recipient_allowlist.simf"), + ), + ( + "capped_debit.simf", + include_str!("../../examples/policy/capped_debit.simf"), + ), + ( + "approvals_2_of_3.simf", + include_str!("../../examples/policy/approvals_2_of_3.simf"), + ), + ( + "approvals_2_of_3_per_input.simf", + include_str!("../../examples/policy/approvals_2_of_3_per_input.simf"), + ), + ( + "constrained_source.simf", + include_str!("../../examples/policy/constrained_source.simf"), + ), + ]; + assert_eq!(bundle.templates.len(), sources.len()); + for (name, source) in sources { + let template = &bundle.templates[name]; + assert_eq!(template.profile_hash, bundle.profile_hash); + assert_eq!( + template.source_sha256, + hex::encode(sha256::Hash::hash(source.as_bytes()).to_byte_array()), + "Stale compiled source for {name}" + ); + assert_eq!( + template.cmr_hex, + policy_cmr(&program(name)).unwrap().to_string(), + "Stale compiled CMR for {name}" + ); + if let Some(duplicate) = &template.duplicate_keys_program_base64 { + let bytes = BASE64_STANDARD.decode(duplicate).unwrap(); + assert_eq!( + template.duplicate_keys_cmr_hex.as_deref(), + Some(policy_cmr(&bytes).unwrap().to_string().as_str()), + "Stale duplicate-key mutation CMR for {name}" + ); + } + } +} + +#[test] +fn allowlist_checks_every_output_including_zero_value_and_last_slot() { + let bytes = program("recipient_allowlist.simf"); + for count in [1, 8] { + let mut fixture = Fixture::new(bytes.clone(), 1, &vec![(10_000, recipient()); count]); + fixture.prepare(0).unwrap().evaluate_default(&[]).unwrap(); + let last = fixture.psbt.unsigned_tx.output.last_mut().unwrap(); + last.value = Amount::ZERO; + let mut script = last.script_pubkey.to_bytes(); + *script.last_mut().unwrap() ^= 1; + last.script_pubkey = ScriptBuf::from_bytes(script); + rejects(&fixture.prepare(0).unwrap(), &[]); + } + let returning = Fixture::new(bytes.clone(), 1, &[(99_000, Destination::Return)]); + returning.prepare(0).unwrap().evaluate_default(&[]).unwrap(); + let excess = Fixture::new(bytes, 1, &vec![(1_000, recipient()); 9]); + assert!(excess.prepare(0).is_err()); +} + +#[test] +fn debit_and_fee_caps_use_all_eight_inputs_and_inclusive_satoshi_boundaries() { + let bytes = program("capped_debit.simf"); + let mut outputs = vec![(14_000, recipient()); 7]; + outputs.push((700_000, Destination::Return)); + let mut fixture = Fixture::new(bytes.clone(), 8, &outputs); + let prepared = fixture.prepare(7).unwrap(); + assert_eq!(prepared.summary().fee, 2_000); + assert_eq!(prepared.summary().debit, 100_000); + prepared.evaluate_default(&[]).unwrap(); + + // Same total outputs/fee, but one satoshi more leaves the common script. + fixture.psbt.unsigned_tx.output[0].value = Amount::from_sat(14_001); + fixture.psbt.unsigned_tx.output[7].value = Amount::from_sat(699_999); + rejects(&fixture.prepare(7).unwrap(), &[]); + + // Same debit, one satoshi above the absolute fee limit. + fixture.psbt.unsigned_tx.output[0].value = Amount::from_sat(13_999); + fixture.psbt.unsigned_tx.output[7].value = Amount::from_sat(700_000); + rejects(&fixture.prepare(7).unwrap(), &[]); + + let aggregate = Fixture::new( + bytes, + 2, + &[(99_000, recipient()), (99_000, Destination::Return)], + ); + assert_eq!(aggregate.prepare(0).unwrap().summary().debit, 101_000); + rejects(&aggregate.prepare(0).unwrap(), &[]); +} + +#[test] +fn approval_threshold_requires_distinct_keys_and_checks_every_present_signature() { + let fixture = Fixture::new( + program("approvals_2_of_3.simf"), + 2, + &[(199_000, Destination::Return)], + ); + let prepared = fixture.prepare(0).unwrap(); + let digest = prepared.context().policy_transaction_approval_hash(); + let [sig1, sig2, sig3] = [1, 2, 3].map(|key| signature(digest, key)); + prepared + .evaluate_default(&signature_witness(&[Some(sig1), Some(sig2), None])) + .unwrap(); + prepared + .evaluate_default(&signature_witness(&[Some(sig1), Some(sig2), Some(sig3)])) + .unwrap(); + rejects(&prepared, &signature_witness(&[Some(sig1), None, None])); + rejects( + &prepared, + &signature_witness(&[Some(sig1), Some(sig1), None]), + ); + rejects( + &prepared, + &signature_witness(&[Some(sig1), Some(sig2), Some(sig1)]), + ); +} + +#[test] +fn duplicate_committed_keys_cannot_inflate_either_approval_threshold() { + for name in ["approvals_2_of_3.simf", "approvals_2_of_3_per_input.simf"] { + let bundle = templates(); + let duplicate_program = BASE64_STANDARD + .decode( + bundle.templates[name] + .duplicate_keys_program_base64 + .as_ref() + .expect("Compiler must include the duplicate-key mutation fixture"), + ) + .unwrap(); + let fixture = Fixture::new(duplicate_program, 1, &[(99_000, Destination::Return)]); + let prepared = fixture.prepare(0).unwrap(); + let digest = if name.contains("per_input") { + prepared.context().policy_approval_hash() + } else { + prepared.context().policy_transaction_approval_hash() + }; + let repeated = signature(digest, 1); + rejects( + &prepared, + &signature_witness(&[Some(repeated), Some(repeated), None]), + ); + } +} + +#[test] +fn transaction_and_input_approval_scopes_reject_relevant_mutations() { + for (name, input_scoped) in [ + ("approvals_2_of_3.simf", false), + ("approvals_2_of_3_per_input.simf", true), + ] { + let mut fixture = Fixture::new(program(name), 2, &[(199_000, Destination::Return)]); + let first = fixture.prepare(0).unwrap(); + let witness = approval_witness(&first, input_scoped); + first.evaluate_default(&witness).unwrap(); + let second = fixture.prepare(1).unwrap(); + if input_scoped { + rejects(&second, &witness); + second + .evaluate_default(&approval_witness(&second, true)) + .unwrap(); + } else { + second.evaluate_default(&witness).unwrap(); + } + let original = fixture.psbt.clone(); + fixture.psbt.unsigned_tx.output[0].value = Amount::from_sat(198_999); + rejects(&fixture.prepare(0).unwrap(), &witness); + fixture.psbt = original.clone(); + let mut bytes = fixture.psbt.unsigned_tx.output[0].script_pubkey.to_bytes(); + *bytes.last_mut().unwrap() ^= 1; + fixture.psbt.unsigned_tx.output[0].script_pubkey = ScriptBuf::from_bytes(bytes); + rejects(&fixture.prepare(0).unwrap(), &witness); + fixture.psbt = original.clone(); + fixture.psbt.unsigned_tx.input[1].sequence = Sequence::from_height(17); + rejects(&fixture.prepare(0).unwrap(), &witness); + fixture.psbt = original.clone(); + fixture.psbt.unsigned_tx.lock_time = btc::absolute::LockTime::from_consensus(1001); + rejects(&fixture.prepare(0).unwrap(), &witness); + fixture.psbt = original; + let mut parent = fixture.psbt.inputs[0].non_witness_utxo.clone().unwrap(); + parent.output[1].value = Amount::from_sat(100_001); + fixture.update_parent(parent); + rejects(&fixture.prepare(0).unwrap(), &witness); + } +} + +#[test] +fn source_constraints_check_all_inputs_csv_cltv_script_bytes_and_authorization() { + let one_input = Fixture::new( + program("constrained_source.simf"), + 1, + &[(99_000, Destination::Return)], + ); + let prepared = one_input.prepare(0).unwrap(); + prepared + .evaluate_default(&source_witness(&prepared)) + .unwrap(); + let mut fixture = Fixture::new( + program("constrained_source.simf"), + 8, + &vec![(99_000, Destination::Return); 8], + ); + let prepared = fixture.prepare(0).unwrap(); + let witness = source_witness(&prepared); + prepared.evaluate_default(&witness).unwrap(); + fixture + .prepare(7) + .unwrap() + .evaluate_default(&witness) + .unwrap(); + let original = fixture.psbt.clone(); + for sequence in [Sequence::MAX, Sequence::from_height(15)] { + fixture.psbt.unsigned_tx.input[7].sequence = sequence; + rejects(&fixture.prepare(0).unwrap(), &witness); + } + fixture.psbt = original.clone(); + for lock_time in [999, 500_000_000] { + fixture.psbt.unsigned_tx.lock_time = btc::absolute::LockTime::from_consensus(lock_time); + rejects(&fixture.prepare(0).unwrap(), &witness); + } + fixture.psbt = original.clone(); + let mut script = fixture.psbt.unsigned_tx.output[7].script_pubkey.to_bytes(); + script[0] = 0x52; + fixture.psbt.unsigned_tx.output[7].script_pubkey = ScriptBuf::from_bytes(script); + rejects(&fixture.prepare(0).unwrap(), &witness); + fixture.psbt = original.clone(); + let mut damaged = witness.clone(); + damaged[1] ^= 1; + rejects(&fixture.prepare(0).unwrap(), &damaged); + + // Even an authenticated source cannot exceed the committed vout restriction. + fixture.psbt = original; + let mut parent = fixture.psbt.inputs[0].non_witness_utxo.clone().unwrap(); + parent.output.push(parent.output[0].clone()); + fixture.psbt.unsigned_tx.input[7].previous_output.vout = 8; + fixture.update_parent(parent); + let prepared = fixture.prepare(0).unwrap(); + rejects(&prepared, &source_witness(&prepared)); +} + +fn live_cli(args: &[String]) -> serde_json::Value { + let output = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(args) + .output() + .unwrap(); + assert!( + output.status.success(), + "stdout: {}\nstderr: {}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr) + ); + serde_json::from_slice(&output.stdout).unwrap() +} + +#[test] +#[ignore = "requires an explicitly configured unfunded testnet4 policy service"] +fn live_cli_compiled_policy_prepare_approve_cosign_and_verified_finalize() { + let service = std::env::var("SIMPLICITY_TEST_SERVICE_URL") + .expect("Set SIMPLICITY_TEST_SERVICE_URL to an unfunded testnet4 policy service"); + let capabilities = live_cli(&[ + "policy".into(), + "capabilities".into(), + "--network".into(), + "testnet4".into(), + "--service".into(), + service.clone(), + ]); + // This local integration fixture deliberately discovers the ephemeral test signer's key. + let base = XOnlyPublicKey::from_slice( + &hex::decode(capabilities["base_public_key_hex"].as_str().unwrap()).unwrap(), + ) + .unwrap(); + let program = program("approvals_2_of_3.simf"); + let tweaked = live_cli(&[ + "policy".into(), + "tweak".into(), + "--network".into(), + "testnet4".into(), + "--program".into(), + BASE64_STANDARD.encode(&program), + "--redeem-script-hex".into(), + "52".into(), + "--base-public-key".into(), + base.to_string(), + "--service".into(), + service.clone(), + ]); + let fixture = Fixture::with_base(program, 2, &[(199_000, Destination::Return)], base); + let prepared = fixture.prepare(0).unwrap(); + assert_eq!( + tweaked["public_key_hex"], + prepared.context().service_pubkey().to_string() + ); + let witness = BASE64_STANDARD.encode(approval_witness(&prepared, false)); + let policy_args = |operation: &str, psbt: &str, index: usize| { + let mut args = vec![ + "policy".into(), + operation.into(), + "--network".into(), + "testnet4".into(), + "--profile-hash".into(), + hex::encode(profile_hash()), + "--program".into(), + BASE64_STANDARD.encode(&fixture.program), + "--redeem-script-hex".into(), + hex::encode(&fixture.script), + "--psbt".into(), + psbt.into(), + "--input-index".into(), + index.to_string(), + "--user-pubkey".into(), + scalar_key(42).x_only_public_key().0.to_string(), + "--user-leaf-hash-hex".into(), + hex::encode([43; 32]), + "--base-public-key".into(), + base.to_string(), + "--offline".into(), + ]; + if matches!(operation, "prepare" | "evaluate" | "sign") { + args.extend(["--service".into(), service.clone()]); + } + if matches!(operation, "evaluate" | "sign") { + args.extend(["--witness".into(), witness.clone()]); + } + args + }; + let mut psbt_hex = hex::encode(fixture.psbt.serialize()); + let local = live_cli(&policy_args("approval-hash", &psbt_hex, 0)); + let remote = live_cli(&policy_args("prepare", &psbt_hex, 0)); + assert_eq!(local, remote); + assert_eq!( + remote["context"], + serde_json::to_value(prepared.summary()).unwrap() + ); + let evaluated = live_cli(&policy_args("evaluate", &psbt_hex, 0)); + assert_eq!(evaluated["accepted"], true); + assert_eq!(evaluated["context"], remote["context"]); + for index in 0..2 { + let signed = live_cli(&policy_args("sign", &psbt_hex, index)); + psbt_hex = signed["psbt_hex"].as_str().unwrap().to_owned(); + } + let mut user_secret = [0; 32]; + user_secret[31] = 42; + let key_file = common::SecretFile::new(user_secret); + for index in 0..2 { + let mut args = policy_args("user-sign", &psbt_hex, index); + args.extend([ + "--secret-key-file".into(), + key_file.0.to_str().unwrap().into(), + ]); + let signed = live_cli(&args); + psbt_hex = signed["psbt_hex"].as_str().unwrap().to_owned(); + } + let finalized = live_cli(&policy_args("finalize", &psbt_hex, 0)); + assert_eq!(finalized["finalized"], true); + let tx: Transaction = btc::consensus::deserialize( + &hex::decode(finalized["transaction_hex"].as_str().unwrap()).unwrap(), + ) + .unwrap(); + assert!(tx.input.iter().all(|input| input.witness.len() == 4)); + assert_eq!(tx.output, fixture.psbt.unsigned_tx.output); + + let mut changed = fixture.psbt.clone(); + changed.unsigned_tx.output[0].value = Amount::from_sat(198_999); + let denied = reqwest::blocking::Client::new() + .post(format!( + "{service}/simplicity-unchained/policy/bitcoin/sign" + )) + .timeout(Duration::from_secs(30)) + .json(&serde_json::json!({ + "profile_hash": hex::encode(profile_hash()), + "network": "testnet4", + "program": BASE64_STANDARD.encode(&fixture.program), + "redeem_script_hex": hex::encode(&fixture.script), + "psbt_hex": hex::encode(changed.serialize()), + "input_index": 0, + "user_pubkey": scalar_key(42).x_only_public_key().0.to_string(), + "user_leaf_hash_hex": hex::encode([43;32]), + "witness": witness, + })) + .send() + .unwrap(); + assert!(!denied.status().is_success()); + let body: serde_json::Value = denied.json().unwrap(); + assert!(body.get("error").is_some()); + assert!(body.get("signature_hex").is_none()); +} diff --git a/cli/tests/public_surface.rs b/cli/tests/public_surface.rs new file mode 100644 index 0000000..31e4d0c --- /dev/null +++ b/cli/tests/public_surface.rs @@ -0,0 +1,74 @@ +use std::process::Command; + +#[test] +fn primary_help_is_policy_only() { + let output = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .arg("--help") + .output() + .unwrap(); + assert!(output.status.success()); + let help = String::from_utf8(output.stdout).unwrap(); + assert!(help.contains("policy")); + assert!(help.contains("keypair")); + assert!(help.contains("liquid-policy")); + for removed in ["btc-tx", "Elements", "address ", "tx "] { + assert!(!help.contains(removed), "{help}"); + } +} + +#[test] +fn legacy_command_workflows_are_unavailable() { + for namespace in ["tx", "btc-tx"] { + for operation in [ + "create", + "sign", + "finalize", + "approval-hash", + "build-csv-leaf", + "spend-user-leaf", + "finalize-user-leaf", + ] { + let output = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args([namespace, operation, "--help"]) + .output() + .unwrap(); + assert!(!output.status.success(), "{namespace} {operation}"); + assert!(output.stdout.is_empty()); + assert!(String::from_utf8_lossy(&output.stderr).contains("unrecognized subcommand")); + } + } + let output = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(["address", "multisig", "--help"]) + .output() + .unwrap(); + assert!(!output.status.success()); + let output = Command::new(env!("CARGO_BIN_EXE_simplicity-unchained")) + .args(["btc-tx", "policy", "--help"]) + .output() + .unwrap(); + assert!(!output.status.success()); +} + +#[test] +fn native_demo_help_and_syntax_are_policy_only() { + let script = format!("{}/scripts/demo_btc.sh", env!("CARGO_MANIFEST_DIR")); + assert!( + Command::new("bash") + .args(["-n", &script]) + .status() + .unwrap() + .success() + ); + let output = Command::new("bash") + .args([&script, "--help"]) + .output() + .unwrap(); + assert!(output.status.success()); + let help = String::from_utf8(output.stdout).unwrap(); + assert!(help.contains("policy-v1")); + assert!(help.contains("testnet4")); + assert!(help.contains("never requests a signature")); + for removed in ["btc-tx", "Liquid", "regtest", "/sign/psbt", "/sign/pset"] { + assert!(!help.contains(removed), "{help}"); + } +} diff --git a/core/Cargo.toml b/core/Cargo.toml index 6b7a6bc..f9725b4 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -10,6 +10,12 @@ path = "src/lib.rs" [dependencies] hex-literal = "1.1.0" -hal-simplicity = { workspace = true } +simplicity-lang = { workspace = true } thiserror = { workspace = true } hex = { workspace = true } +dlopen2 = "0.8.2" +jet_plugin_abi = { path = "../jet_plugin_abi" } +simplicity-unchained-policy = { path = "../policy" } + +[dev-dependencies] +script_jets = { path = "../plugin_tests/script_jets" } diff --git a/core/README.md b/core/README.md index e64e8f7..4872a4a 100644 --- a/core/README.md +++ b/core/README.md @@ -1,12 +1,83 @@ # Simplicity Unchained Core -Contains the fundamental logic for executing Simplicity programs and interacting with the Elements blockchain. +Contains the fundamental logic for executing Simplicity policies with Bitcoin and Elements environments. ## Capabilities -- Execute Simplicity programs with provided data and environment. -- Utilities for interacting with Elements and Simplicity environments. -- Custom execution environment supporting the injection of custom jets. +- Execute `bitcoin-policy-v1` policies against verified PSBT context. +- Execute [`liquid-policy-v1`](../policy/LIQUID.md) policies using the portable VM and proof-verified PSET context. +- Provide cryptographic, Taproot and transaction-environment utilities. +- Load [custom jet plugins](src/plugin/README.md) for native callers. The policy profiles use fixed registries. + +## How It Works + +### The Unchained Model + +The service commits to a Simplicity policy through its Taproot key tweak, without requiring Simplicity as an on-chain consensus rule. + +1. The user writes and compiles a policy for the intended profile, choosing its keys and script data before funding. +2. The service computes the program's **CMR** (Commitment Merkle Root). +3. The service derives a cooperative key that commits to the profile fingerprint, network genesis hash, CMR and exact script-data bytes. +4. The user constructs a P2TR address from that key, their own key and a recovery leaf hash. + +Keep the base signing key and published profile available for funded outputs. Enforcement is off-chain: someone holding the signing key can bypass policy evaluation. + +### Taproot Tree Structure + +```plaintext +P2TR output +├── Internal key: standard NUMS (unspendable) +└── Script tree + ├── Leaf 0 — Co-signing leaf (2-of-2 multisig) + │ OP_CHECKSIG + │ OP_CHECKSIGADD OP_2 OP_EQUAL + └── Leaf 1 — User recovery leaf (hidden from the service) +``` + +**Leaf 0 — Co-signing leaf**: Both the service and user must sign. The service key commits to the policy; the service checks that each input spends the reconstructed output before signing. + +**Leaf 1 — User recovery leaf**: The user chooses a recovery script, such as a CSV timelock and user key, and keeps the full script privately. The service needs only its hash. A valid recovery spend does not require service cooperation or policy evaluation. + +### Co-signing Flow + +1. The user prepares a PSBT/PSET with full parent transactions, the committed program and script data, their public key and the recovery hash. +2. The service verifies parent txids, referenced outputs and `witness_utxo` data, then reconstructs every input's policy-bound output. Liquid also requires verified asset/value disclosures and transaction proofs. +3. The user reviews the transaction context and obtains any approvals required by the policy. +4. The service evaluates the witnessed program and, if it passes, returns a cooperative script-path signature for the selected input. +5. The user verifies the service signature, adds their own signature and finalizes after both signatures are present for every input. + +Parent verification proves transaction content, not confirmation or unspentness. Both profiles report `chain_status_verified: false`; check live chain status separately. + +### Bitcoin Per-Spend Approvals + +The 55 Bitcoin policy jets expose transaction amounts, output scripts, debit including fees, declared locktime conditions and approval hashes: + +- J53 `policy_approval_hash` binds the transaction context, selected input and its cooperative sighash. +- J54 `policy_transaction_approval_hash` permits one approval across all same-contract inputs of the same transaction. + +Use [`policy approval-hash`](../cli/README.md#prepare-approve-and-co-sign) to reconstruct the context locally from full parents and a known base service key. Sign the decoded 32-byte digest directly with BIP-340 and compile the signatures into the policy witness. Changing the approved transaction or context requires fresh approvals. + +A policy must verify a transaction-dependent condition to authorize individual spends. Static hashlocks and fixed-message signatures are replayable; debit and fee caps apply per transaction, not per day. See the [policy templates](../examples/policy/README.md) and [approval preimages](../policy/README.md#commitments-and-approval-preimages). + +### Script Commitment + +For Bitcoin, the cooperative key is derived as follows: + +```text +commitment = TaggedHash("SimplicityUnchained/PolicySigningCommitment/v1", + profile_hash || genesis_hash || CMR || SHA256(exact_script_data)) +service_key = BIP341 TapTweak(base_service_key, root = commitment) +``` + +The [portable kernel](../policy/README.md) defines the registry, fingerprint, context verification and approval preimages. The [Liquid profile](../policy/LIQUID.md) defines its asset semantics and genesis-bound signing rules. + +## Execution API + +For Bitcoin, use `policy::policy_cmr` to decode a program without its witness, then `policy::prepare_policy` or `PreparedPolicy::new` to validate the transaction and obtain an immutable `context()` and serializable `summary()`. + +The profile accepts version 2, one to eight same-contract inputs and one to eight outputs, with SIGHASH_DEFAULT script-path signing. It rejects duplicate inputs, missing or conflicting parents, overspending and finalized witnesses. + +`evaluate_default(witness)` uses a 10,000,000-milliweight cap. `evaluate(witness, Cost)` and `evaluate_milliweight(witness, u32)` can lower it. Cost is checked before BitMachine allocation and is a static bound, not elapsed time. Errors distinguish decoding, invalid context, policy rejection, execution limits and VM failure. Signing prepares and evaluates again rather than trusting a previous evaluation. ## Licence diff --git a/core/src/jets/base.rs b/core/src/jets/base.rs new file mode 100644 index 0000000..c688359 --- /dev/null +++ b/core/src/jets/base.rs @@ -0,0 +1,149 @@ +//! Runs base jets on an environment that wraps their own. +//! +//! `JetEnvironment::c_jet_ptr` must return a plain `fn` pointer, so a base jet can't be wrapped in +//! a closure that swaps the outer env for the base C env. Instead, every base jet gets its own +//! trampoline, a `fn` monomorphized over the jet's index in `ALL`. +use simplicity::ffi::CFrameItem; +use simplicity::jet::{Core, CoreEnv, Elements, ElementsTxEnv, Jet, JetEnvironment}; + +use super::bitcoin::CoreExtension; +use super::elements::ElementsExtension; +use super::environments::{BitcoinUnchainedEnv, ElementsUnchainedEnv, UnchainedEnv}; + +/// C jet function running on an environment of type `W`. +pub type JetFn = fn(&mut CFrameItem, CFrameItem, &W) -> bool; + +/// Upper bound on the number of jets in a base jet set, see [`fn_table`]. +const MAX_BASE_JETS: usize = 512; + +/// A base jet set together with the environment its C implementations run on. +pub trait BaseJet: Jet + Copy { + type Env: JetEnvironment; + + /// All jets of the set, indexed by [`BaseJet::index`]. + const ALL: &'static [Self]; + + /// Position of `self` inside [`BaseJet::ALL`]. + fn index(&self) -> usize; +} + +/// An environment that holds the environment of a base jet set. +pub trait AsBaseEnv { + fn base_env(&self) -> &E; +} + +impl AsBaseEnv for UnchainedEnv { + fn base_env(&self) -> &E { + &self.env + } +} + +impl AsBaseEnv for BitcoinUnchainedEnv { + fn base_env(&self) -> &CoreEnv { + &self.env.core_env + } +} + +impl BaseJet for Core { + type Env = CoreEnv; + const ALL: &'static [Self] = &Core::ALL; + + fn index(&self) -> usize { + *self as usize + } +} + +impl BaseJet for Elements { + type Env = ElementsTxEnv; + const ALL: &'static [Self] = &Elements::ALL; + + fn index(&self) -> usize { + *self as usize + } +} + +impl BaseJet for CoreExtension { + type Env = BitcoinUnchainedEnv; + const ALL: &'static [Self] = &CoreExtension::ALL; + + fn index(&self) -> usize { + match self { + CoreExtension::Core(inner) => inner.index(), + CoreExtension::GetOpcodeFromScript => Core::ALL.len(), + CoreExtension::GetPubkeyFromScript => Core::ALL.len() + 1, + CoreExtension::TxApprovalHash => Core::ALL.len() + 2, + } + } +} + +impl BaseJet for ElementsExtension { + type Env = ElementsUnchainedEnv; + const ALL: &'static [Self] = &ElementsExtension::ALL; + + fn index(&self) -> usize { + match self { + ElementsExtension::Elements(inner) => inner.index(), + ElementsExtension::GetOpcodeFromScript => Elements::ALL.len(), + ElementsExtension::GetPubkeyFromScript => Elements::ALL.len() + 1, + } + } +} + +/// Returns the C implementation of a base jet, adapted to run on `W`. +pub fn base_c_jet_ptr>(jet: &J) -> JetFn { + const { assert!(J::ALL.len() <= MAX_BASE_JETS) }; + + Trampolines::::TABLE.as_flattened().as_flattened()[jet.index()] +} + +fn trampoline, const I: usize>( + dst: &mut CFrameItem, + src: CFrameItem, + env: &W, +) -> bool { + let c_jet = ::c_jet_ptr(&J::ALL[I]); + c_jet(dst, src, env.base_env().c_jet_env()) +} + +/// Builds a `[[[_; 8]; 8]; 8]` table of `$f::` for every `I` below [`MAX_BASE_JETS`]. +// TODO: refactor into something less ugly, e.g. a flat table generated by a proc macro. +macro_rules! fn_table { + ($f:ident) => { + fn_table!(@a $f; 0 1 2 3 4 5 6 7) + }; + (@a $f:ident; $($a:literal)*) => { + [$(fn_table!(@b $f; $a; 0 1 2 3 4 5 6 7)),*] + }; + (@b $f:ident; $a:literal; $($b:literal)*) => { + [$(fn_table!(@c $f; $a; $b; 0 1 2 3 4 5 6 7)),*] + }; + (@c $f:ident; $a:literal; $b:literal; $($c:literal)*) => { + [$($f:: as JetFn),*] + }; +} + +struct Trampolines(J, W); + +impl> Trampolines { + #[allow(clippy::type_complexity)] + const TABLE: [[[JetFn; 8]; 8]; 8] = fn_table!(trampoline); +} + +#[cfg(test)] +mod tests { + use super::*; + + fn assert_indices() { + for (i, jet) in J::ALL.iter().enumerate() { + assert_eq!(jet.index(), i, "{jet}"); + } + } + + #[test] + fn index_matches_position_in_all() { + assert_indices::(); + assert_indices::(); + assert_indices::(); + assert_indices::(); + } +} diff --git a/core/src/jets/bitcoin.rs b/core/src/jets/bitcoin.rs index 09a7aaa..feb3420 100644 --- a/core/src/jets/bitcoin.rs +++ b/core/src/jets/bitcoin.rs @@ -1,13 +1,15 @@ use std::io::Write; +use std::mem::MaybeUninit; -use hal_simplicity::simplicity::Cmr; -use hal_simplicity::simplicity::Cost; -use hal_simplicity::simplicity::ffi::CFrameItem; -use hal_simplicity::simplicity::jet::type_name::TypeName; -use hal_simplicity::simplicity::jet::{Core, Jet}; -use hal_simplicity::simplicity::{BitIter, BitWriter, decode}; +use simplicity::Cmr; +use simplicity::Cost; +use simplicity::ffi::CFrameItem; +use simplicity::jet::type_name::TypeName; +use simplicity::jet::{Core, Jet, JetEnvironment}; +use simplicity::{BitIter, BitWriter, decode}; -use super::environments::UnchainedEnv; +use super::base::base_c_jet_ptr; +use super::environments::{BitcoinTxEnv, BitcoinUnchainedEnv}; // Local version of decode_bits macro that accepts expressions instead of just paths macro_rules! decode_bits { @@ -31,393 +33,87 @@ pub enum CoreExtension { Core(Core), GetOpcodeFromScript, GetPubkeyFromScript, + TxApprovalHash, } impl CoreExtension { - pub const ALL: [Self; 370] = [ - CoreExtension::Core(Core::Add16), - CoreExtension::Core(Core::Add32), - CoreExtension::Core(Core::Add64), - CoreExtension::Core(Core::Add8), - CoreExtension::Core(Core::All16), - CoreExtension::Core(Core::All32), - CoreExtension::Core(Core::All64), - CoreExtension::Core(Core::All8), - CoreExtension::Core(Core::And1), - CoreExtension::Core(Core::And16), - CoreExtension::Core(Core::And32), - CoreExtension::Core(Core::And64), - CoreExtension::Core(Core::And8), - CoreExtension::Core(Core::Bip0340Verify), - CoreExtension::Core(Core::Ch1), - CoreExtension::Core(Core::Ch16), - CoreExtension::Core(Core::Ch32), - CoreExtension::Core(Core::Ch64), - CoreExtension::Core(Core::Ch8), - CoreExtension::Core(Core::CheckSigVerify), - CoreExtension::Core(Core::Complement1), - CoreExtension::Core(Core::Complement16), - CoreExtension::Core(Core::Complement32), - CoreExtension::Core(Core::Complement64), - CoreExtension::Core(Core::Complement8), - CoreExtension::Core(Core::Decompress), - CoreExtension::Core(Core::Decrement16), - CoreExtension::Core(Core::Decrement32), - CoreExtension::Core(Core::Decrement64), - CoreExtension::Core(Core::Decrement8), - CoreExtension::Core(Core::DivMod128_64), - CoreExtension::Core(Core::DivMod16), - CoreExtension::Core(Core::DivMod32), - CoreExtension::Core(Core::DivMod64), - CoreExtension::Core(Core::DivMod8), - CoreExtension::Core(Core::Divide16), - CoreExtension::Core(Core::Divide32), - CoreExtension::Core(Core::Divide64), - CoreExtension::Core(Core::Divide8), - CoreExtension::Core(Core::Divides16), - CoreExtension::Core(Core::Divides32), - CoreExtension::Core(Core::Divides64), - CoreExtension::Core(Core::Divides8), - CoreExtension::Core(Core::Eq1), - CoreExtension::Core(Core::Eq16), - CoreExtension::Core(Core::Eq256), - CoreExtension::Core(Core::Eq32), - CoreExtension::Core(Core::Eq64), - CoreExtension::Core(Core::Eq8), - CoreExtension::Core(Core::FeAdd), - CoreExtension::Core(Core::FeInvert), - CoreExtension::Core(Core::FeIsOdd), - CoreExtension::Core(Core::FeIsZero), - CoreExtension::Core(Core::FeMultiply), - CoreExtension::Core(Core::FeMultiplyBeta), - CoreExtension::Core(Core::FeNegate), - CoreExtension::Core(Core::FeNormalize), - CoreExtension::Core(Core::FeSquare), - CoreExtension::Core(Core::FeSquareRoot), - CoreExtension::Core(Core::FullAdd16), - CoreExtension::Core(Core::FullAdd32), - CoreExtension::Core(Core::FullAdd64), - CoreExtension::Core(Core::FullAdd8), - CoreExtension::Core(Core::FullDecrement16), - CoreExtension::Core(Core::FullDecrement32), - CoreExtension::Core(Core::FullDecrement64), - CoreExtension::Core(Core::FullDecrement8), - CoreExtension::Core(Core::FullIncrement16), - CoreExtension::Core(Core::FullIncrement32), - CoreExtension::Core(Core::FullIncrement64), - CoreExtension::Core(Core::FullIncrement8), - CoreExtension::Core(Core::FullLeftShift16_1), - CoreExtension::Core(Core::FullLeftShift16_2), - CoreExtension::Core(Core::FullLeftShift16_4), - CoreExtension::Core(Core::FullLeftShift16_8), - CoreExtension::Core(Core::FullLeftShift32_1), - CoreExtension::Core(Core::FullLeftShift32_16), - CoreExtension::Core(Core::FullLeftShift32_2), - CoreExtension::Core(Core::FullLeftShift32_4), - CoreExtension::Core(Core::FullLeftShift32_8), - CoreExtension::Core(Core::FullLeftShift64_1), - CoreExtension::Core(Core::FullLeftShift64_16), - CoreExtension::Core(Core::FullLeftShift64_2), - CoreExtension::Core(Core::FullLeftShift64_32), - CoreExtension::Core(Core::FullLeftShift64_4), - CoreExtension::Core(Core::FullLeftShift64_8), - CoreExtension::Core(Core::FullLeftShift8_1), - CoreExtension::Core(Core::FullLeftShift8_2), - CoreExtension::Core(Core::FullLeftShift8_4), - CoreExtension::Core(Core::FullMultiply16), - CoreExtension::Core(Core::FullMultiply32), - CoreExtension::Core(Core::FullMultiply64), - CoreExtension::Core(Core::FullMultiply8), - CoreExtension::Core(Core::FullRightShift16_1), - CoreExtension::Core(Core::FullRightShift16_2), - CoreExtension::Core(Core::FullRightShift16_4), - CoreExtension::Core(Core::FullRightShift16_8), - CoreExtension::Core(Core::FullRightShift32_1), - CoreExtension::Core(Core::FullRightShift32_16), - CoreExtension::Core(Core::FullRightShift32_2), - CoreExtension::Core(Core::FullRightShift32_4), - CoreExtension::Core(Core::FullRightShift32_8), - CoreExtension::Core(Core::FullRightShift64_1), - CoreExtension::Core(Core::FullRightShift64_16), - CoreExtension::Core(Core::FullRightShift64_2), - CoreExtension::Core(Core::FullRightShift64_32), - CoreExtension::Core(Core::FullRightShift64_4), - CoreExtension::Core(Core::FullRightShift64_8), - CoreExtension::Core(Core::FullRightShift8_1), - CoreExtension::Core(Core::FullRightShift8_2), - CoreExtension::Core(Core::FullRightShift8_4), - CoreExtension::Core(Core::FullSubtract16), - CoreExtension::Core(Core::FullSubtract32), - CoreExtension::Core(Core::FullSubtract64), - CoreExtension::Core(Core::FullSubtract8), - CoreExtension::Core(Core::GeIsOnCurve), - CoreExtension::Core(Core::GeNegate), - CoreExtension::Core(Core::GejAdd), - CoreExtension::Core(Core::GejDouble), - CoreExtension::Core(Core::GejEquiv), - CoreExtension::Core(Core::GejGeAdd), - CoreExtension::Core(Core::GejGeAddEx), - CoreExtension::Core(Core::GejGeEquiv), - CoreExtension::Core(Core::GejInfinity), - CoreExtension::Core(Core::GejIsInfinity), - CoreExtension::Core(Core::GejIsOnCurve), - CoreExtension::Core(Core::GejNegate), - CoreExtension::Core(Core::GejNormalize), - CoreExtension::Core(Core::GejRescale), - CoreExtension::Core(Core::GejXEquiv), - CoreExtension::Core(Core::GejYIsOdd), - CoreExtension::Core(Core::Generate), - CoreExtension::Core(Core::HashToCurve), - CoreExtension::Core(Core::High1), - CoreExtension::Core(Core::High16), - CoreExtension::Core(Core::High32), - CoreExtension::Core(Core::High64), - CoreExtension::Core(Core::High8), - CoreExtension::Core(Core::Increment16), - CoreExtension::Core(Core::Increment32), - CoreExtension::Core(Core::Increment64), - CoreExtension::Core(Core::Increment8), - CoreExtension::Core(Core::IsOne16), - CoreExtension::Core(Core::IsOne32), - CoreExtension::Core(Core::IsOne64), - CoreExtension::Core(Core::IsOne8), - CoreExtension::Core(Core::IsZero16), - CoreExtension::Core(Core::IsZero32), - CoreExtension::Core(Core::IsZero64), - CoreExtension::Core(Core::IsZero8), - CoreExtension::Core(Core::Le16), - CoreExtension::Core(Core::Le32), - CoreExtension::Core(Core::Le64), - CoreExtension::Core(Core::Le8), - CoreExtension::Core(Core::LeftExtend16_32), - CoreExtension::Core(Core::LeftExtend16_64), - CoreExtension::Core(Core::LeftExtend1_16), - CoreExtension::Core(Core::LeftExtend1_32), - CoreExtension::Core(Core::LeftExtend1_64), - CoreExtension::Core(Core::LeftExtend1_8), - CoreExtension::Core(Core::LeftExtend32_64), - CoreExtension::Core(Core::LeftExtend8_16), - CoreExtension::Core(Core::LeftExtend8_32), - CoreExtension::Core(Core::LeftExtend8_64), - CoreExtension::Core(Core::LeftPadHigh16_32), - CoreExtension::Core(Core::LeftPadHigh16_64), - CoreExtension::Core(Core::LeftPadHigh1_16), - CoreExtension::Core(Core::LeftPadHigh1_32), - CoreExtension::Core(Core::LeftPadHigh1_64), - CoreExtension::Core(Core::LeftPadHigh1_8), - CoreExtension::Core(Core::LeftPadHigh32_64), - CoreExtension::Core(Core::LeftPadHigh8_16), - CoreExtension::Core(Core::LeftPadHigh8_32), - CoreExtension::Core(Core::LeftPadHigh8_64), - CoreExtension::Core(Core::LeftPadLow16_32), - CoreExtension::Core(Core::LeftPadLow16_64), - CoreExtension::Core(Core::LeftPadLow1_16), - CoreExtension::Core(Core::LeftPadLow1_32), - CoreExtension::Core(Core::LeftPadLow1_64), - CoreExtension::Core(Core::LeftPadLow1_8), - CoreExtension::Core(Core::LeftPadLow32_64), - CoreExtension::Core(Core::LeftPadLow8_16), - CoreExtension::Core(Core::LeftPadLow8_32), - CoreExtension::Core(Core::LeftPadLow8_64), - CoreExtension::Core(Core::LeftRotate16), - CoreExtension::Core(Core::LeftRotate32), - CoreExtension::Core(Core::LeftRotate64), - CoreExtension::Core(Core::LeftRotate8), - CoreExtension::Core(Core::LeftShift16), - CoreExtension::Core(Core::LeftShift32), - CoreExtension::Core(Core::LeftShift64), - CoreExtension::Core(Core::LeftShift8), - CoreExtension::Core(Core::LeftShiftWith16), - CoreExtension::Core(Core::LeftShiftWith32), - CoreExtension::Core(Core::LeftShiftWith64), - CoreExtension::Core(Core::LeftShiftWith8), - CoreExtension::Core(Core::Leftmost16_1), - CoreExtension::Core(Core::Leftmost16_2), - CoreExtension::Core(Core::Leftmost16_4), - CoreExtension::Core(Core::Leftmost16_8), - CoreExtension::Core(Core::Leftmost32_1), - CoreExtension::Core(Core::Leftmost32_16), - CoreExtension::Core(Core::Leftmost32_2), - CoreExtension::Core(Core::Leftmost32_4), - CoreExtension::Core(Core::Leftmost32_8), - CoreExtension::Core(Core::Leftmost64_1), - CoreExtension::Core(Core::Leftmost64_16), - CoreExtension::Core(Core::Leftmost64_2), - CoreExtension::Core(Core::Leftmost64_32), - CoreExtension::Core(Core::Leftmost64_4), - CoreExtension::Core(Core::Leftmost64_8), - CoreExtension::Core(Core::Leftmost8_1), - CoreExtension::Core(Core::Leftmost8_2), - CoreExtension::Core(Core::Leftmost8_4), - CoreExtension::Core(Core::LinearCombination1), - CoreExtension::Core(Core::LinearVerify1), - CoreExtension::Core(Core::Low1), - CoreExtension::Core(Core::Low16), - CoreExtension::Core(Core::Low32), - CoreExtension::Core(Core::Low64), - CoreExtension::Core(Core::Low8), - CoreExtension::Core(Core::Lt16), - CoreExtension::Core(Core::Lt32), - CoreExtension::Core(Core::Lt64), - CoreExtension::Core(Core::Lt8), - CoreExtension::Core(Core::Maj1), - CoreExtension::Core(Core::Maj16), - CoreExtension::Core(Core::Maj32), - CoreExtension::Core(Core::Maj64), - CoreExtension::Core(Core::Maj8), - CoreExtension::Core(Core::Max16), - CoreExtension::Core(Core::Max32), - CoreExtension::Core(Core::Max64), - CoreExtension::Core(Core::Max8), - CoreExtension::Core(Core::Median16), - CoreExtension::Core(Core::Median32), - CoreExtension::Core(Core::Median64), - CoreExtension::Core(Core::Median8), - CoreExtension::Core(Core::Min16), - CoreExtension::Core(Core::Min32), - CoreExtension::Core(Core::Min64), - CoreExtension::Core(Core::Min8), - CoreExtension::Core(Core::Modulo16), - CoreExtension::Core(Core::Modulo32), - CoreExtension::Core(Core::Modulo64), - CoreExtension::Core(Core::Modulo8), - CoreExtension::Core(Core::Multiply16), - CoreExtension::Core(Core::Multiply32), - CoreExtension::Core(Core::Multiply64), - CoreExtension::Core(Core::Multiply8), - CoreExtension::Core(Core::Negate16), - CoreExtension::Core(Core::Negate32), - CoreExtension::Core(Core::Negate64), - CoreExtension::Core(Core::Negate8), - CoreExtension::Core(Core::One16), - CoreExtension::Core(Core::One32), - CoreExtension::Core(Core::One64), - CoreExtension::Core(Core::One8), - CoreExtension::Core(Core::Or1), - CoreExtension::Core(Core::Or16), - CoreExtension::Core(Core::Or32), - CoreExtension::Core(Core::Or64), - CoreExtension::Core(Core::Or8), - CoreExtension::Core(Core::ParseLock), - CoreExtension::Core(Core::ParseSequence), - CoreExtension::Core(Core::PointVerify1), - CoreExtension::Core(Core::RightExtend16_32), - CoreExtension::Core(Core::RightExtend16_64), - CoreExtension::Core(Core::RightExtend32_64), - CoreExtension::Core(Core::RightExtend8_16), - CoreExtension::Core(Core::RightExtend8_32), - CoreExtension::Core(Core::RightExtend8_64), - CoreExtension::Core(Core::RightPadHigh16_32), - CoreExtension::Core(Core::RightPadHigh16_64), - CoreExtension::Core(Core::RightPadHigh1_16), - CoreExtension::Core(Core::RightPadHigh1_32), - CoreExtension::Core(Core::RightPadHigh1_64), - CoreExtension::Core(Core::RightPadHigh1_8), - CoreExtension::Core(Core::RightPadHigh32_64), - CoreExtension::Core(Core::RightPadHigh8_16), - CoreExtension::Core(Core::RightPadHigh8_32), - CoreExtension::Core(Core::RightPadHigh8_64), - CoreExtension::Core(Core::RightPadLow16_32), - CoreExtension::Core(Core::RightPadLow16_64), - CoreExtension::Core(Core::RightPadLow1_16), - CoreExtension::Core(Core::RightPadLow1_32), - CoreExtension::Core(Core::RightPadLow1_64), - CoreExtension::Core(Core::RightPadLow1_8), - CoreExtension::Core(Core::RightPadLow32_64), - CoreExtension::Core(Core::RightPadLow8_16), - CoreExtension::Core(Core::RightPadLow8_32), - CoreExtension::Core(Core::RightPadLow8_64), - CoreExtension::Core(Core::RightRotate16), - CoreExtension::Core(Core::RightRotate32), - CoreExtension::Core(Core::RightRotate64), - CoreExtension::Core(Core::RightRotate8), - CoreExtension::Core(Core::RightShift16), - CoreExtension::Core(Core::RightShift32), - CoreExtension::Core(Core::RightShift64), - CoreExtension::Core(Core::RightShift8), - CoreExtension::Core(Core::RightShiftWith16), - CoreExtension::Core(Core::RightShiftWith32), - CoreExtension::Core(Core::RightShiftWith64), - CoreExtension::Core(Core::RightShiftWith8), - CoreExtension::Core(Core::Rightmost16_1), - CoreExtension::Core(Core::Rightmost16_2), - CoreExtension::Core(Core::Rightmost16_4), - CoreExtension::Core(Core::Rightmost16_8), - CoreExtension::Core(Core::Rightmost32_1), - CoreExtension::Core(Core::Rightmost32_16), - CoreExtension::Core(Core::Rightmost32_2), - CoreExtension::Core(Core::Rightmost32_4), - CoreExtension::Core(Core::Rightmost32_8), - CoreExtension::Core(Core::Rightmost64_1), - CoreExtension::Core(Core::Rightmost64_16), - CoreExtension::Core(Core::Rightmost64_2), - CoreExtension::Core(Core::Rightmost64_32), - CoreExtension::Core(Core::Rightmost64_4), - CoreExtension::Core(Core::Rightmost64_8), - CoreExtension::Core(Core::Rightmost8_1), - CoreExtension::Core(Core::Rightmost8_2), - CoreExtension::Core(Core::Rightmost8_4), - CoreExtension::Core(Core::ScalarAdd), - CoreExtension::Core(Core::ScalarInvert), - CoreExtension::Core(Core::ScalarIsZero), - CoreExtension::Core(Core::ScalarMultiply), - CoreExtension::Core(Core::ScalarMultiplyLambda), - CoreExtension::Core(Core::ScalarNegate), - CoreExtension::Core(Core::ScalarNormalize), - CoreExtension::Core(Core::ScalarSquare), - CoreExtension::Core(Core::Scale), - CoreExtension::Core(Core::Sha256Block), - CoreExtension::Core(Core::Sha256Ctx8Add1), - CoreExtension::Core(Core::Sha256Ctx8Add128), - CoreExtension::Core(Core::Sha256Ctx8Add16), - CoreExtension::Core(Core::Sha256Ctx8Add2), - CoreExtension::Core(Core::Sha256Ctx8Add256), - CoreExtension::Core(Core::Sha256Ctx8Add32), - CoreExtension::Core(Core::Sha256Ctx8Add4), - CoreExtension::Core(Core::Sha256Ctx8Add512), - CoreExtension::Core(Core::Sha256Ctx8Add64), - CoreExtension::Core(Core::Sha256Ctx8Add8), - CoreExtension::Core(Core::Sha256Ctx8AddBuffer511), - CoreExtension::Core(Core::Sha256Ctx8Finalize), - CoreExtension::Core(Core::Sha256Ctx8Init), - CoreExtension::Core(Core::Sha256Iv), - CoreExtension::Core(Core::Some1), - CoreExtension::Core(Core::Some16), - CoreExtension::Core(Core::Some32), - CoreExtension::Core(Core::Some64), - CoreExtension::Core(Core::Some8), - CoreExtension::Core(Core::Subtract16), - CoreExtension::Core(Core::Subtract32), - CoreExtension::Core(Core::Subtract64), - CoreExtension::Core(Core::Subtract8), - CoreExtension::Core(Core::Swu), - CoreExtension::Core(Core::TapdataInit), - CoreExtension::Core(Core::Verify), - CoreExtension::Core(Core::Xor1), - CoreExtension::Core(Core::Xor16), - CoreExtension::Core(Core::Xor32), - CoreExtension::Core(Core::Xor64), - CoreExtension::Core(Core::Xor8), - CoreExtension::Core(Core::XorXor1), - CoreExtension::Core(Core::XorXor16), - CoreExtension::Core(Core::XorXor32), - CoreExtension::Core(Core::XorXor64), - CoreExtension::Core(Core::XorXor8), - CoreExtension::GetOpcodeFromScript, - CoreExtension::GetPubkeyFromScript, - ]; + pub const ALL: [Self; Self::ALL_JETS_NUM] = Self::build_all_variants(); + + const ALL_JETS_NUM: usize = Core::ALL.len() + 3; + + const fn build_all_variants() -> [Self; Self::ALL_JETS_NUM] { + struct AllVariantsBuilder { + data: [MaybeUninit; CoreExtension::ALL_JETS_NUM], + len: usize, + } + + impl AllVariantsBuilder { + const fn new() -> Self { + Self { + data: [MaybeUninit::uninit(); CoreExtension::ALL_JETS_NUM], + len: 0, + } + } + + const fn push(&mut self, item: CoreExtension) { + assert!(self.len < self.data.len()); + + self.data[self.len].write(item); + self.len += 1; + } + + const fn finalize(self) -> [CoreExtension; CoreExtension::ALL_JETS_NUM] { + assert!(self.len == CoreExtension::ALL_JETS_NUM); + + unsafe { std::mem::transmute(self.data) } + } + } + + let mut builder = AllVariantsBuilder::new(); + let mut i = 0; + + while i < Core::ALL.len() { + builder.push(CoreExtension::Core(Core::ALL[i])); + i += 1; + } + + builder.push(CoreExtension::GetOpcodeFromScript); + builder.push(CoreExtension::GetPubkeyFromScript); + builder.push(CoreExtension::TxApprovalHash); + + builder.finalize() + } } -impl Jet for CoreExtension { - type Environment = UnchainedEnv<()>; - type CJetEnvironment = UnchainedEnv<()>; +impl JetEnvironment for BitcoinUnchainedEnv { + type Jet = CoreExtension; + type CJetEnvironment = Self; - fn c_jet_env(env: &Self::Environment) -> &Self::CJetEnvironment { + fn c_jet_env(&self) -> &Self::CJetEnvironment { // For the time being, we are goint to use the initial environment for unchained jets, // as we are going to implement them in rust. - env + self } + fn c_jet_ptr( + jet: &Self::Jet, + ) -> fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool { + match jet { + // rest of elements jets + CoreExtension::Core(inner_jet) => base_c_jet_ptr(inner_jet), + // custom jets + CoreExtension::GetOpcodeFromScript => { + super::exec::get_opcode_from_script:: + } + CoreExtension::GetPubkeyFromScript => { + super::exec::get_pubkey_from_script:: + } + CoreExtension::TxApprovalHash => super::exec::bitcoin_tx_approval_hash, + } + } +} + +impl Jet for CoreExtension { fn cmr(&self) -> Cmr { if let CoreExtension::Core(inner_jet) = self { return inner_jet.cmr(); @@ -434,6 +130,9 @@ impl Jet for CoreExtension { 0xd2, 0x52, 0x0e, 0xcc, 0x02, 0x09, 0x00, 0x67, 0xc8, 0xfc, 0xca, 0xbb, 0x4d, 0x72, 0xa6, 0x55, 0xcd, 0xcb, ], + CoreExtension::TxApprovalHash => hex_literal::hex!( + "f0ed96ee8fbf1a69beb20deb3a7a9b748bdc18da284d15438debc7d4faeb9962" + ), _ => unreachable!(), }; @@ -448,6 +147,7 @@ impl Jet for CoreExtension { let name = match self { CoreExtension::GetOpcodeFromScript => b"c", CoreExtension::GetPubkeyFromScript => b"c", + CoreExtension::TxApprovalHash => b"1", _ => unreachable!(), }; @@ -462,2322 +162,89 @@ impl Jet for CoreExtension { let name = match self { CoreExtension::GetOpcodeFromScript => b"c", CoreExtension::GetPubkeyFromScript => b"h", + CoreExtension::TxApprovalHash => b"h", _ => unreachable!(), }; TypeName(name) } - fn encode(&self, w: &mut BitWriter) -> std::io::Result { + fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { if let CoreExtension::Core(inner_jet) = self { return inner_jet.encode(w); } let (n, len) = match self { - CoreExtension::GetOpcodeFromScript => (30, 5), - CoreExtension::GetPubkeyFromScript => (62, 6), + CoreExtension::GetOpcodeFromScript => (62, 6), + CoreExtension::GetPubkeyFromScript => (126, 7), + CoreExtension::TxApprovalHash => (127, 7), _ => unreachable!(), }; w.write_bits_be(n, len) } + /// # Safety + /// + /// Due to the lack of a `Clone` bound on `I`, the underlying implementation uses + /// `ptr::read` to create bitwise copies of the iterator. This is unsafe and may cause + /// undefined behavior if `I` contains types that manage unique resources. + /// This works correctly for common slice-based iterators like `Copied>`. + /// + /// See for details. fn decode>(bits: &mut BitIter) -> Result { - decode_bits!(bits, { - 0 => { - 0 => {CoreExtension::Core(Core::Verify)}, + let (mut core_iter, mut custom_iter) = + unsafe { (std::ptr::read(bits), std::ptr::read(bits)) }; + + let bits_read = bits.n_total_read(); + + let try_elements = Core::decode(&mut core_iter); + + if let Ok(jet) = try_elements { + for _ in 0..(core_iter.n_total_read() - bits_read) { + bits.next(); + } + + std::mem::forget(core_iter); + std::mem::forget(custom_iter); + + return Ok(CoreExtension::Core(jet)); + } + + let custom_iter_ref = &mut custom_iter; + let try_custom = decode_bits!(custom_iter_ref, { + 0 => {}, + 1 => { + 0 => {}, 1 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Low1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Low8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Low16)}, - 1 => {CoreExtension::Core(Core::Low32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Low64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::High1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::High8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::High16)}, - 1 => {CoreExtension::Core(Core::High32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::High64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, + 0 => {}, 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Complement1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Complement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Complement16)}, - 1 => {CoreExtension::Core(Core::Complement32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Complement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::And1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::And8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::And16)}, - 1 => {CoreExtension::Core(Core::And32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::And64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Or1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Or8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Or16)}, - 1 => {CoreExtension::Core(Core::Or32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Or64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::Xor1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Xor8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Xor16)}, - 1 => {CoreExtension::Core(Core::Xor32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Xor64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Maj1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Maj8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Maj16)}, - 1 => {CoreExtension::Core(Core::Maj32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Maj64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::XorXor1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::XorXor8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::XorXor16)}, - 1 => {CoreExtension::Core(Core::XorXor32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::XorXor64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Ch1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Ch8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Ch16)}, - 1 => {CoreExtension::Core(Core::Ch32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Ch64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::Some1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Some8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Some16)}, - 1 => {CoreExtension::Core(Core::Some32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Some64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::All8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::All16)}, - 1 => {CoreExtension::Core(Core::All32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::All64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::Eq1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Eq8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Eq16)}, - 1 => {CoreExtension::Core(Core::Eq32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Eq64)}, - 1 => {} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Eq256)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FullLeftShift8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift16_1)}, - 1 => {CoreExtension::Core(Core::FullLeftShift32_1)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullLeftShift64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift8_2)}, - 1 => {CoreExtension::Core(Core::FullLeftShift16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift32_2)}, - 1 => {CoreExtension::Core(Core::FullLeftShift64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullLeftShift8_4)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift16_4)}, - 1 => {CoreExtension::Core(Core::FullLeftShift32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift16_8)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift32_8)}, - 1 => {CoreExtension::Core(Core::FullLeftShift64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullLeftShift32_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullLeftShift64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FullRightShift8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift16_1)}, - 1 => {CoreExtension::Core(Core::FullRightShift32_1)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullRightShift64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift8_2)}, - 1 => {CoreExtension::Core(Core::FullRightShift16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift32_2)}, - 1 => {CoreExtension::Core(Core::FullRightShift64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullRightShift8_4)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift16_4)}, - 1 => {CoreExtension::Core(Core::FullRightShift32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift16_8)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift32_8)}, - 1 => {CoreExtension::Core(Core::FullRightShift64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullRightShift32_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullRightShift64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Leftmost8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost16_1)}, - 1 => {CoreExtension::Core(Core::Leftmost32_1)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Leftmost64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost8_2)}, - 1 => {CoreExtension::Core(Core::Leftmost16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost32_2)}, - 1 => {CoreExtension::Core(Core::Leftmost64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::Leftmost8_4)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost16_4)}, - 1 => {CoreExtension::Core(Core::Leftmost32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost16_8)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost32_8)}, - 1 => {CoreExtension::Core(Core::Leftmost64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::Leftmost32_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Leftmost64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Rightmost8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost16_1)}, - 1 => {CoreExtension::Core(Core::Rightmost32_1)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Rightmost64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost8_2)}, - 1 => {CoreExtension::Core(Core::Rightmost16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost32_2)}, - 1 => {CoreExtension::Core(Core::Rightmost64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::Rightmost8_4)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost16_4)}, - 1 => {CoreExtension::Core(Core::Rightmost32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost16_8)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost32_8)}, - 1 => {CoreExtension::Core(Core::Rightmost64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::Rightmost32_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Rightmost64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::LeftPadLow1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadLow1_16)}, - 1 => {CoreExtension::Core(Core::LeftPadLow1_32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftPadLow1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadLow8_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadLow8_32)}, - 1 => {CoreExtension::Core(Core::LeftPadLow8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftPadLow16_32)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadLow16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadLow32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::LeftPadHigh1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadHigh1_16)}, - 1 => {CoreExtension::Core(Core::LeftPadHigh1_32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftPadHigh1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadHigh8_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadHigh8_32)}, - 1 => {CoreExtension::Core(Core::LeftPadHigh8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftPadHigh16_32)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadHigh16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftPadHigh32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::LeftExtend1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftExtend1_16)}, - 1 => {CoreExtension::Core(Core::LeftExtend1_32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftExtend1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftExtend8_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftExtend8_32)}, - 1 => {CoreExtension::Core(Core::LeftExtend8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftExtend16_32)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftExtend16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftExtend32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::RightPadLow1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadLow1_16)}, - 1 => {CoreExtension::Core(Core::RightPadLow1_32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightPadLow1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadLow8_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadLow8_32)}, - 1 => {CoreExtension::Core(Core::RightPadLow8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightPadLow16_32)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadLow16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadLow32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::RightPadHigh1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadHigh1_16)}, - 1 => {CoreExtension::Core(Core::RightPadHigh1_32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightPadHigh1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadHigh8_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadHigh8_32)}, - 1 => {CoreExtension::Core(Core::RightPadHigh8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightPadHigh16_32)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadHigh16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightPadHigh32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightExtend8_16)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightExtend8_32)}, - 1 => {CoreExtension::Core(Core::RightExtend8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightExtend16_32)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightExtend16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightExtend32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::LeftShiftWith8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftShiftWith16)}, - 1 => {CoreExtension::Core(Core::LeftShiftWith32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftShiftWith64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::RightShiftWith8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightShiftWith16)}, - 1 => {CoreExtension::Core(Core::RightShiftWith32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightShiftWith64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::LeftShift8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftShift16)}, - 1 => {CoreExtension::Core(Core::LeftShift32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftShift64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::RightShift8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightShift16)}, - 1 => {CoreExtension::Core(Core::RightShift32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightShift64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::LeftRotate8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LeftRotate16)}, - 1 => {CoreExtension::Core(Core::LeftRotate32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::LeftRotate64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::RightRotate8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::RightRotate16)}, - 1 => {CoreExtension::Core(Core::RightRotate32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::RightRotate64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => {} - } - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::One8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::One16)}, - 1 => {CoreExtension::Core(Core::One32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::One64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, + 0 => {}, 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FullAdd8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullAdd16)}, - 1 => {CoreExtension::Core(Core::FullAdd32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullAdd64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Add8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Add16)}, - 1 => {CoreExtension::Core(Core::Add32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Add64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FullIncrement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullIncrement16)}, - 1 => {CoreExtension::Core(Core::FullIncrement32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullIncrement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Increment8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Increment16)}, - 1 => {CoreExtension::Core(Core::Increment32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Increment64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FullSubtract8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullSubtract16)}, - 1 => {CoreExtension::Core(Core::FullSubtract32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullSubtract64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Subtract8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Subtract16)}, - 1 => {CoreExtension::Core(Core::Subtract32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Subtract64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Negate8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Negate16)}, - 1 => {CoreExtension::Core(Core::Negate32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Negate64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FullDecrement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullDecrement16)}, - 1 => {CoreExtension::Core(Core::FullDecrement32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullDecrement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Decrement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Decrement16)}, - 1 => {CoreExtension::Core(Core::Decrement32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Decrement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FullMultiply8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FullMultiply16)}, - 1 => {CoreExtension::Core(Core::FullMultiply32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FullMultiply64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Multiply8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Multiply16)}, - 1 => {CoreExtension::Core(Core::Multiply32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Multiply64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::IsZero8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::IsZero16)}, - 1 => {CoreExtension::Core(Core::IsZero32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::IsZero64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::IsOne8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::IsOne16)}, - 1 => {CoreExtension::Core(Core::IsOne32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::IsOne64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Le8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Le16)}, - 1 => {CoreExtension::Core(Core::Le32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Le64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Lt8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Lt16)}, - 1 => {CoreExtension::Core(Core::Lt32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Lt64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Min8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Min16)}, - 1 => {CoreExtension::Core(Core::Min32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Min64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Max8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Max16)}, - 1 => {CoreExtension::Core(Core::Max32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Max64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Median8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Median16)}, - 1 => {CoreExtension::Core(Core::Median32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Median64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => {CoreExtension::Core(Core::DivMod128_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::DivMod8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::DivMod16)}, - 1 => {CoreExtension::Core(Core::DivMod32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::DivMod64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Divide8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Divide16)}, - 1 => {CoreExtension::Core(Core::Divide32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Divide64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Modulo8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Modulo16)}, - 1 => {CoreExtension::Core(Core::Modulo32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Modulo64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::Divides8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Divides16)}, - 1 => {CoreExtension::Core(Core::Divides32)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Divides64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Sha256Block)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Sha256Iv)}, - 1 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8Add1)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8Add2)}, - 1 => {CoreExtension::Core(Core::Sha256Ctx8Add4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8Add8)}, - 1 => {CoreExtension::Core(Core::Sha256Ctx8Add16)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8Add32)}, - 1 => {CoreExtension::Core(Core::Sha256Ctx8Add64)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8Add128)}, - 1 => {CoreExtension::Core(Core::Sha256Ctx8Add256)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8Add512)}, - 1 => {} - } - }, - 1 => {} - } - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8AddBuffer511)}, - 1 => {CoreExtension::Core(Core::Sha256Ctx8Finalize)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Sha256Ctx8Init)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => {} - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::PointVerify1)}, - 1 => {} - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Decompress)}, - 1 => { - 0 => {CoreExtension::Core(Core::LinearVerify1)}, - 1 => {} - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::LinearCombination1)}, - 1 => {} - }, - 1 => {CoreExtension::Core(Core::Scale)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::Generate)}, - 1 => {CoreExtension::Core(Core::GejInfinity)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::GejNormalize)}, - 1 => {CoreExtension::Core(Core::GejNegate)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::GeNegate)}, - 1 => {CoreExtension::Core(Core::GejDouble)} - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::GejAdd)}, - 1 => {CoreExtension::Core(Core::GejGeAddEx)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::GejGeAdd)}, - 1 => {CoreExtension::Core(Core::GejRescale)} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::GejIsInfinity)}, - 1 => {CoreExtension::Core(Core::GejEquiv)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::GejGeEquiv)}, - 1 => {CoreExtension::Core(Core::GejXEquiv)} - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::GejYIsOdd)}, - 1 => {CoreExtension::Core(Core::GejIsOnCurve)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::GeIsOnCurve)}, - 1 => {CoreExtension::Core(Core::ScalarNormalize)} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::ScalarNegate)}, - 1 => {CoreExtension::Core(Core::ScalarAdd)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::ScalarSquare)}, - 1 => {CoreExtension::Core(Core::ScalarMultiply)} - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::ScalarMultiplyLambda)}, - 1 => {CoreExtension::Core(Core::ScalarInvert)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::ScalarIsZero)}, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => {CoreExtension::Core(Core::FeNormalize)} - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FeNegate)}, - 1 => {CoreExtension::Core(Core::FeAdd)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FeSquare)}, - 1 => {CoreExtension::Core(Core::FeMultiply)} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {CoreExtension::Core(Core::FeMultiplyBeta)}, - 1 => {CoreExtension::Core(Core::FeInvert)} - }, - 1 => { - 0 => {CoreExtension::Core(Core::FeSquareRoot)}, - 1 => {CoreExtension::Core(Core::FeIsZero)} - } - }, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::FeIsOdd)}, - 1 => {} - }, - 1 => { - 0 => {CoreExtension::Core(Core::HashToCurve)}, - 1 => {CoreExtension::Core(Core::Swu)} - } - } - } - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => {CoreExtension::Core(Core::CheckSigVerify)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::Bip0340Verify)}, - 1 => {} - }, - 1 => {} - } - } - }, + 0 => {}, // Free path 1 => { - 0 => {}, + 0 => {CoreExtension::GetOpcodeFromScript}, 1 => { - 0 => {CoreExtension::Core(Core::ParseLock)}, - 1 => { - 0 => { - 0 => {CoreExtension::Core(Core::ParseSequence)}, - 1 => {CoreExtension::Core(Core::TapdataInit)} - }, - 1 => {} - } + 0 => {CoreExtension::GetPubkeyFromScript}, + 1 => {CoreExtension::TxApprovalHash} } } - }, - 1 => {} - }, - 1 => { - 0 => {}, // Free path - 1 => { - 0 => {CoreExtension::GetOpcodeFromScript}, - 1 => { - 0 => {CoreExtension::GetPubkeyFromScript}, - 1 => {} - } } } } } - }) - } + }); - fn c_jet_ptr(&self) -> &dyn Fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool { - match self { - CoreExtension::Core(inner_jet) => jet_wrapper(*inner_jet), - CoreExtension::GetOpcodeFromScript => &super::exec::get_opcode_from_script, - CoreExtension::GetPubkeyFromScript => &super::exec::get_pubkey_from_script, + if try_custom.is_ok() { + for _ in 0..(custom_iter.n_total_read() - bits_read) { + bits.next(); + } } + + std::mem::forget(core_iter); + std::mem::forget(custom_iter); + + try_custom } fn cost(&self) -> Cost { @@ -2789,9 +256,14 @@ impl Jet for CoreExtension { match self { CoreExtension::GetOpcodeFromScript => Cost::from_milliweight(100), CoreExtension::GetPubkeyFromScript => Cost::from_milliweight(100), + CoreExtension::TxApprovalHash => Cost::from_milliweight(356), _ => unreachable!(), } } + + fn parse(s: &str) -> Result { + s.parse() + } } impl std::fmt::Display for CoreExtension { @@ -2800,17 +272,19 @@ impl std::fmt::Display for CoreExtension { CoreExtension::Core(inner_jet) => f.write_str(&inner_jet.to_string()), CoreExtension::GetOpcodeFromScript => f.write_str("get_opcode_from_script"), CoreExtension::GetPubkeyFromScript => f.write_str("get_pubkey_from_script"), + CoreExtension::TxApprovalHash => f.write_str("tx_approval_hash"), } } } impl std::str::FromStr for CoreExtension { - type Err = hal_simplicity::simplicity::Error; + type Err = simplicity::Error; fn from_str(s: &str) -> Result { match s { "get_opcode_from_script" => Ok(CoreExtension::GetOpcodeFromScript), "get_pubkey_from_script" => Ok(CoreExtension::GetPubkeyFromScript), + "tx_approval_hash" => Ok(CoreExtension::TxApprovalHash), _ => { let inner_jet = s.parse::()?; Ok(CoreExtension::Core(inner_jet)) @@ -2818,404 +292,3 @@ impl std::str::FromStr for CoreExtension { } } } - -// Macro to generate static wrapper functions AND dispatcher for Core jets -// This macro generates both the wrapper functions and the match statement in one go, -// so we only need to list each Core variant once. -macro_rules! jet_wrappers { - ($($variant:ident),* $(,)?) => { - // Generate individual wrapper functions for each variant - $( - #[allow(non_snake_case)] - fn $variant(frame: &mut CFrameItem, arg: CFrameItem, _env: &UnchainedEnv<()>) -> bool { - Core::$variant.c_jet_ptr()(frame, arg, &()) - } - )* - - // Generate the dispatcher function that returns the appropriate wrapper - fn jet_wrapper(jet: Core) -> &'static dyn Fn(&mut CFrameItem, CFrameItem, &UnchainedEnv<()>) -> bool { - match jet { - $( - Core::$variant => &$variant, - )* - } - } - }; -} - -// Generate wrapper functions and dispatcher for all Core jet variants -// If Core enum changes, only update this list to keep wrappers in sync -// -// TODO(ivanlele): This is extremly ungly solution, will need to open an issue -// for `rust-simplicity` to better interface for jet trait that -// does not require this boilerplate -jet_wrappers! { - Add16, - Add32, - Add64, - Add8, - All16, - All32, - All64, - All8, - And1, - And16, - And32, - And64, - And8, - Bip0340Verify, - Ch1, - Ch16, - Ch32, - Ch64, - Ch8, - CheckSigVerify, - Complement1, - Complement16, - Complement32, - Complement64, - Complement8, - Decompress, - Decrement16, - Decrement32, - Decrement64, - Decrement8, - DivMod128_64, - DivMod16, - DivMod32, - DivMod64, - DivMod8, - Divide16, - Divide32, - Divide64, - Divide8, - Divides16, - Divides32, - Divides64, - Divides8, - Eq1, - Eq16, - Eq256, - Eq32, - Eq64, - Eq8, - FeAdd, - FeInvert, - FeIsOdd, - FeIsZero, - FeMultiply, - FeMultiplyBeta, - FeNegate, - FeNormalize, - FeSquare, - FeSquareRoot, - FullAdd16, - FullAdd32, - FullAdd64, - FullAdd8, - FullDecrement16, - FullDecrement32, - FullDecrement64, - FullDecrement8, - FullIncrement16, - FullIncrement32, - FullIncrement64, - FullIncrement8, - FullLeftShift16_1, - FullLeftShift16_2, - FullLeftShift16_4, - FullLeftShift16_8, - FullLeftShift32_1, - FullLeftShift32_16, - FullLeftShift32_2, - FullLeftShift32_4, - FullLeftShift32_8, - FullLeftShift64_1, - FullLeftShift64_16, - FullLeftShift64_2, - FullLeftShift64_32, - FullLeftShift64_4, - FullLeftShift64_8, - FullLeftShift8_1, - FullLeftShift8_2, - FullLeftShift8_4, - FullMultiply16, - FullMultiply32, - FullMultiply64, - FullMultiply8, - FullRightShift16_1, - FullRightShift16_2, - FullRightShift16_4, - FullRightShift16_8, - FullRightShift32_1, - FullRightShift32_16, - FullRightShift32_2, - FullRightShift32_4, - FullRightShift32_8, - FullRightShift64_1, - FullRightShift64_16, - FullRightShift64_2, - FullRightShift64_32, - FullRightShift64_4, - FullRightShift64_8, - FullRightShift8_1, - FullRightShift8_2, - FullRightShift8_4, - FullSubtract16, - FullSubtract32, - FullSubtract64, - FullSubtract8, - GeIsOnCurve, - GeNegate, - GejAdd, - GejDouble, - GejEquiv, - GejGeAdd, - GejGeAddEx, - GejGeEquiv, - GejInfinity, - GejIsInfinity, - GejIsOnCurve, - GejNegate, - GejNormalize, - GejRescale, - GejXEquiv, - GejYIsOdd, - Generate, - HashToCurve, - High1, - High16, - High32, - High64, - High8, - Increment16, - Increment32, - Increment64, - Increment8, - IsOne16, - IsOne32, - IsOne64, - IsOne8, - IsZero16, - IsZero32, - IsZero64, - IsZero8, - Le16, - Le32, - Le64, - Le8, - LeftExtend16_32, - LeftExtend16_64, - LeftExtend1_16, - LeftExtend1_32, - LeftExtend1_64, - LeftExtend1_8, - LeftExtend32_64, - LeftExtend8_16, - LeftExtend8_32, - LeftExtend8_64, - LeftPadHigh16_32, - LeftPadHigh16_64, - LeftPadHigh1_16, - LeftPadHigh1_32, - LeftPadHigh1_64, - LeftPadHigh1_8, - LeftPadHigh32_64, - LeftPadHigh8_16, - LeftPadHigh8_32, - LeftPadHigh8_64, - LeftPadLow16_32, - LeftPadLow16_64, - LeftPadLow1_16, - LeftPadLow1_32, - LeftPadLow1_64, - LeftPadLow1_8, - LeftPadLow32_64, - LeftPadLow8_16, - LeftPadLow8_32, - LeftPadLow8_64, - LeftRotate16, - LeftRotate32, - LeftRotate64, - LeftRotate8, - LeftShift16, - LeftShift32, - LeftShift64, - LeftShift8, - LeftShiftWith16, - LeftShiftWith32, - LeftShiftWith64, - LeftShiftWith8, - Leftmost16_1, - Leftmost16_2, - Leftmost16_4, - Leftmost16_8, - Leftmost32_1, - Leftmost32_16, - Leftmost32_2, - Leftmost32_4, - Leftmost32_8, - Leftmost64_1, - Leftmost64_16, - Leftmost64_2, - Leftmost64_32, - Leftmost64_4, - Leftmost64_8, - Leftmost8_1, - Leftmost8_2, - Leftmost8_4, - LinearCombination1, - LinearVerify1, - Low1, - Low16, - Low32, - Low64, - Low8, - Lt16, - Lt32, - Lt64, - Lt8, - Maj1, - Maj16, - Maj32, - Maj64, - Maj8, - Max16, - Max32, - Max64, - Max8, - Median16, - Median32, - Median64, - Median8, - Min16, - Min32, - Min64, - Min8, - Modulo16, - Modulo32, - Modulo64, - Modulo8, - Multiply16, - Multiply32, - Multiply64, - Multiply8, - Negate16, - Negate32, - Negate64, - Negate8, - One16, - One32, - One64, - One8, - Or1, - Or16, - Or32, - Or64, - Or8, - ParseLock, - ParseSequence, - PointVerify1, - RightExtend16_32, - RightExtend16_64, - RightExtend32_64, - RightExtend8_16, - RightExtend8_32, - RightExtend8_64, - RightPadHigh16_32, - RightPadHigh16_64, - RightPadHigh1_16, - RightPadHigh1_32, - RightPadHigh1_64, - RightPadHigh1_8, - RightPadHigh32_64, - RightPadHigh8_16, - RightPadHigh8_32, - RightPadHigh8_64, - RightPadLow16_32, - RightPadLow16_64, - RightPadLow1_16, - RightPadLow1_32, - RightPadLow1_64, - RightPadLow1_8, - RightPadLow32_64, - RightPadLow8_16, - RightPadLow8_32, - RightPadLow8_64, - RightRotate16, - RightRotate32, - RightRotate64, - RightRotate8, - RightShift16, - RightShift32, - RightShift64, - RightShift8, - RightShiftWith16, - RightShiftWith32, - RightShiftWith64, - RightShiftWith8, - Rightmost16_1, - Rightmost16_2, - Rightmost16_4, - Rightmost16_8, - Rightmost32_1, - Rightmost32_16, - Rightmost32_2, - Rightmost32_4, - Rightmost32_8, - Rightmost64_1, - Rightmost64_16, - Rightmost64_2, - Rightmost64_32, - Rightmost64_4, - Rightmost64_8, - Rightmost8_1, - Rightmost8_2, - Rightmost8_4, - ScalarAdd, - ScalarInvert, - ScalarIsZero, - ScalarMultiply, - ScalarMultiplyLambda, - ScalarNegate, - ScalarNormalize, - ScalarSquare, - Scale, - Sha256Block, - Sha256Ctx8Add1, - Sha256Ctx8Add128, - Sha256Ctx8Add16, - Sha256Ctx8Add2, - Sha256Ctx8Add256, - Sha256Ctx8Add32, - Sha256Ctx8Add4, - Sha256Ctx8Add512, - Sha256Ctx8Add64, - Sha256Ctx8Add8, - Sha256Ctx8AddBuffer511, - Sha256Ctx8Finalize, - Sha256Ctx8Init, - Sha256Iv, - Some1, - Some16, - Some32, - Some64, - Some8, - Subtract16, - Subtract32, - Subtract64, - Subtract8, - Swu, - TapdataInit, - Verify, - Xor1, - Xor16, - Xor32, - Xor64, - Xor8, - XorXor1, - XorXor16, - XorXor32, - XorXor64, - XorXor8, -} diff --git a/core/src/jets/elements.rs b/core/src/jets/elements.rs index 9ba2050..c389274 100644 --- a/core/src/jets/elements.rs +++ b/core/src/jets/elements.rs @@ -1,17 +1,15 @@ use std::io::Write; -use std::sync::Arc; +use std::mem::MaybeUninit; -use hal_simplicity::simplicity::Cmr; -use hal_simplicity::simplicity::Cost; -use hal_simplicity::simplicity::ffi::CFrameItem; -use hal_simplicity::simplicity::jet::type_name::TypeName; -use hal_simplicity::simplicity::jet::{Elements, Jet}; -use hal_simplicity::simplicity::{BitIter, BitWriter, decode}; +use simplicity::Cmr; +use simplicity::Cost; +use simplicity::ffi::CFrameItem; +use simplicity::jet::type_name::TypeName; +use simplicity::jet::{Elements, ElementsTxEnv, Jet, JetEnvironment}; +use simplicity::{BitIter, BitWriter, decode}; -use hal_simplicity::simplicity::elements::Transaction; -use hal_simplicity::simplicity::jet::elements::ElementsEnv; - -use super::environments::UnchainedEnv; +use super::base::base_c_jet_ptr; +use super::environments::ElementsUnchainedEnv; // Local version of decode_bits macro that accepts expressions instead of just paths macro_rules! decode_bits { @@ -38,493 +36,94 @@ pub enum ElementsExtension { } impl ElementsExtension { - pub const ALL: [Self; 473] = [ - Self::Elements(Elements::Add16), - Self::Elements(Elements::Add32), - Self::Elements(Elements::Add64), - Self::Elements(Elements::Add8), - Self::Elements(Elements::All16), - Self::Elements(Elements::All32), - Self::Elements(Elements::All64), - Self::Elements(Elements::All8), - Self::Elements(Elements::And1), - Self::Elements(Elements::And16), - Self::Elements(Elements::And32), - Self::Elements(Elements::And64), - Self::Elements(Elements::And8), - Self::Elements(Elements::AnnexHash), - Self::Elements(Elements::AssetAmountHash), - Self::Elements(Elements::Bip0340Verify), - Self::Elements(Elements::BuildTapbranch), - Self::Elements(Elements::BuildTapleafSimplicity), - Self::Elements(Elements::BuildTaptweak), - Self::Elements(Elements::CalculateAsset), - Self::Elements(Elements::CalculateConfidentialToken), - Self::Elements(Elements::CalculateExplicitToken), - Self::Elements(Elements::CalculateIssuanceEntropy), - Self::Elements(Elements::Ch1), - Self::Elements(Elements::Ch16), - Self::Elements(Elements::Ch32), - Self::Elements(Elements::Ch64), - Self::Elements(Elements::Ch8), - Self::Elements(Elements::CheckLockDistance), - Self::Elements(Elements::CheckLockDuration), - Self::Elements(Elements::CheckLockHeight), - Self::Elements(Elements::CheckLockTime), - Self::Elements(Elements::CheckSigVerify), - Self::Elements(Elements::Complement1), - Self::Elements(Elements::Complement16), - Self::Elements(Elements::Complement32), - Self::Elements(Elements::Complement64), - Self::Elements(Elements::Complement8), - Self::Elements(Elements::CurrentAmount), - Self::Elements(Elements::CurrentAnnexHash), - Self::Elements(Elements::CurrentAsset), - Self::Elements(Elements::CurrentIndex), - Self::Elements(Elements::CurrentIssuanceAssetAmount), - Self::Elements(Elements::CurrentIssuanceAssetProof), - Self::Elements(Elements::CurrentIssuanceTokenAmount), - Self::Elements(Elements::CurrentIssuanceTokenProof), - Self::Elements(Elements::CurrentNewIssuanceContract), - Self::Elements(Elements::CurrentPegin), - Self::Elements(Elements::CurrentPrevOutpoint), - Self::Elements(Elements::CurrentReissuanceBlinding), - Self::Elements(Elements::CurrentReissuanceEntropy), - Self::Elements(Elements::CurrentScriptHash), - Self::Elements(Elements::CurrentScriptSigHash), - Self::Elements(Elements::CurrentSequence), - Self::Elements(Elements::Decompress), - Self::Elements(Elements::Decrement16), - Self::Elements(Elements::Decrement32), - Self::Elements(Elements::Decrement64), - Self::Elements(Elements::Decrement8), - Self::Elements(Elements::DivMod128_64), - Self::Elements(Elements::DivMod16), - Self::Elements(Elements::DivMod32), - Self::Elements(Elements::DivMod64), - Self::Elements(Elements::DivMod8), - Self::Elements(Elements::Divide16), - Self::Elements(Elements::Divide32), - Self::Elements(Elements::Divide64), - Self::Elements(Elements::Divide8), - Self::Elements(Elements::Divides16), - Self::Elements(Elements::Divides32), - Self::Elements(Elements::Divides64), - Self::Elements(Elements::Divides8), - Self::Elements(Elements::Eq1), - Self::Elements(Elements::Eq16), - Self::Elements(Elements::Eq256), - Self::Elements(Elements::Eq32), - Self::Elements(Elements::Eq64), - Self::Elements(Elements::Eq8), - Self::Elements(Elements::FeAdd), - Self::Elements(Elements::FeInvert), - Self::Elements(Elements::FeIsOdd), - Self::Elements(Elements::FeIsZero), - Self::Elements(Elements::FeMultiply), - Self::Elements(Elements::FeMultiplyBeta), - Self::Elements(Elements::FeNegate), - Self::Elements(Elements::FeNormalize), - Self::Elements(Elements::FeSquare), - Self::Elements(Elements::FeSquareRoot), - Self::Elements(Elements::FullAdd16), - Self::Elements(Elements::FullAdd32), - Self::Elements(Elements::FullAdd64), - Self::Elements(Elements::FullAdd8), - Self::Elements(Elements::FullDecrement16), - Self::Elements(Elements::FullDecrement32), - Self::Elements(Elements::FullDecrement64), - Self::Elements(Elements::FullDecrement8), - Self::Elements(Elements::FullIncrement16), - Self::Elements(Elements::FullIncrement32), - Self::Elements(Elements::FullIncrement64), - Self::Elements(Elements::FullIncrement8), - Self::Elements(Elements::FullLeftShift16_1), - Self::Elements(Elements::FullLeftShift16_2), - Self::Elements(Elements::FullLeftShift16_4), - Self::Elements(Elements::FullLeftShift16_8), - Self::Elements(Elements::FullLeftShift32_1), - Self::Elements(Elements::FullLeftShift32_16), - Self::Elements(Elements::FullLeftShift32_2), - Self::Elements(Elements::FullLeftShift32_4), - Self::Elements(Elements::FullLeftShift32_8), - Self::Elements(Elements::FullLeftShift64_1), - Self::Elements(Elements::FullLeftShift64_16), - Self::Elements(Elements::FullLeftShift64_2), - Self::Elements(Elements::FullLeftShift64_32), - Self::Elements(Elements::FullLeftShift64_4), - Self::Elements(Elements::FullLeftShift64_8), - Self::Elements(Elements::FullLeftShift8_1), - Self::Elements(Elements::FullLeftShift8_2), - Self::Elements(Elements::FullLeftShift8_4), - Self::Elements(Elements::FullMultiply16), - Self::Elements(Elements::FullMultiply32), - Self::Elements(Elements::FullMultiply64), - Self::Elements(Elements::FullMultiply8), - Self::Elements(Elements::FullRightShift16_1), - Self::Elements(Elements::FullRightShift16_2), - Self::Elements(Elements::FullRightShift16_4), - Self::Elements(Elements::FullRightShift16_8), - Self::Elements(Elements::FullRightShift32_1), - Self::Elements(Elements::FullRightShift32_16), - Self::Elements(Elements::FullRightShift32_2), - Self::Elements(Elements::FullRightShift32_4), - Self::Elements(Elements::FullRightShift32_8), - Self::Elements(Elements::FullRightShift64_1), - Self::Elements(Elements::FullRightShift64_16), - Self::Elements(Elements::FullRightShift64_2), - Self::Elements(Elements::FullRightShift64_32), - Self::Elements(Elements::FullRightShift64_4), - Self::Elements(Elements::FullRightShift64_8), - Self::Elements(Elements::FullRightShift8_1), - Self::Elements(Elements::FullRightShift8_2), - Self::Elements(Elements::FullRightShift8_4), - Self::Elements(Elements::FullSubtract16), - Self::Elements(Elements::FullSubtract32), - Self::Elements(Elements::FullSubtract64), - Self::Elements(Elements::FullSubtract8), - Self::Elements(Elements::GeIsOnCurve), - Self::Elements(Elements::GeNegate), - Self::Elements(Elements::GejAdd), - Self::Elements(Elements::GejDouble), - Self::Elements(Elements::GejEquiv), - Self::Elements(Elements::GejGeAdd), - Self::Elements(Elements::GejGeAddEx), - Self::Elements(Elements::GejGeEquiv), - Self::Elements(Elements::GejInfinity), - Self::Elements(Elements::GejIsInfinity), - Self::Elements(Elements::GejIsOnCurve), - Self::Elements(Elements::GejNegate), - Self::Elements(Elements::GejNormalize), - Self::Elements(Elements::GejRescale), - Self::Elements(Elements::GejXEquiv), - Self::Elements(Elements::GejYIsOdd), - Self::Elements(Elements::Generate), - Self::Elements(Elements::GenesisBlockHash), - Self::Elements(Elements::HashToCurve), - Self::Elements(Elements::High1), - Self::Elements(Elements::High16), - Self::Elements(Elements::High32), - Self::Elements(Elements::High64), - Self::Elements(Elements::High8), - Self::Elements(Elements::Increment16), - Self::Elements(Elements::Increment32), - Self::Elements(Elements::Increment64), - Self::Elements(Elements::Increment8), - Self::Elements(Elements::InputAmount), - Self::Elements(Elements::InputAmountsHash), - Self::Elements(Elements::InputAnnexHash), - Self::Elements(Elements::InputAnnexesHash), - Self::Elements(Elements::InputAsset), - Self::Elements(Elements::InputHash), - Self::Elements(Elements::InputOutpointsHash), - Self::Elements(Elements::InputPegin), - Self::Elements(Elements::InputPrevOutpoint), - Self::Elements(Elements::InputScriptHash), - Self::Elements(Elements::InputScriptSigHash), - Self::Elements(Elements::InputScriptSigsHash), - Self::Elements(Elements::InputScriptsHash), - Self::Elements(Elements::InputSequence), - Self::Elements(Elements::InputSequencesHash), - Self::Elements(Elements::InputUtxoHash), - Self::Elements(Elements::InputUtxosHash), - Self::Elements(Elements::InputsHash), - Self::Elements(Elements::InternalKey), - Self::Elements(Elements::IsOne16), - Self::Elements(Elements::IsOne32), - Self::Elements(Elements::IsOne64), - Self::Elements(Elements::IsOne8), - Self::Elements(Elements::IsZero16), - Self::Elements(Elements::IsZero32), - Self::Elements(Elements::IsZero64), - Self::Elements(Elements::IsZero8), - Self::Elements(Elements::Issuance), - Self::Elements(Elements::IssuanceAsset), - Self::Elements(Elements::IssuanceAssetAmount), - Self::Elements(Elements::IssuanceAssetAmountsHash), - Self::Elements(Elements::IssuanceAssetProof), - Self::Elements(Elements::IssuanceBlindingEntropyHash), - Self::Elements(Elements::IssuanceEntropy), - Self::Elements(Elements::IssuanceHash), - Self::Elements(Elements::IssuanceRangeProofsHash), - Self::Elements(Elements::IssuanceToken), - Self::Elements(Elements::IssuanceTokenAmount), - Self::Elements(Elements::IssuanceTokenAmountsHash), - Self::Elements(Elements::IssuanceTokenProof), - Self::Elements(Elements::IssuancesHash), - Self::Elements(Elements::LbtcAsset), - Self::Elements(Elements::Le16), - Self::Elements(Elements::Le32), - Self::Elements(Elements::Le64), - Self::Elements(Elements::Le8), - Self::Elements(Elements::LeftExtend16_32), - Self::Elements(Elements::LeftExtend16_64), - Self::Elements(Elements::LeftExtend1_16), - Self::Elements(Elements::LeftExtend1_32), - Self::Elements(Elements::LeftExtend1_64), - Self::Elements(Elements::LeftExtend1_8), - Self::Elements(Elements::LeftExtend32_64), - Self::Elements(Elements::LeftExtend8_16), - Self::Elements(Elements::LeftExtend8_32), - Self::Elements(Elements::LeftExtend8_64), - Self::Elements(Elements::LeftPadHigh16_32), - Self::Elements(Elements::LeftPadHigh16_64), - Self::Elements(Elements::LeftPadHigh1_16), - Self::Elements(Elements::LeftPadHigh1_32), - Self::Elements(Elements::LeftPadHigh1_64), - Self::Elements(Elements::LeftPadHigh1_8), - Self::Elements(Elements::LeftPadHigh32_64), - Self::Elements(Elements::LeftPadHigh8_16), - Self::Elements(Elements::LeftPadHigh8_32), - Self::Elements(Elements::LeftPadHigh8_64), - Self::Elements(Elements::LeftPadLow16_32), - Self::Elements(Elements::LeftPadLow16_64), - Self::Elements(Elements::LeftPadLow1_16), - Self::Elements(Elements::LeftPadLow1_32), - Self::Elements(Elements::LeftPadLow1_64), - Self::Elements(Elements::LeftPadLow1_8), - Self::Elements(Elements::LeftPadLow32_64), - Self::Elements(Elements::LeftPadLow8_16), - Self::Elements(Elements::LeftPadLow8_32), - Self::Elements(Elements::LeftPadLow8_64), - Self::Elements(Elements::LeftRotate16), - Self::Elements(Elements::LeftRotate32), - Self::Elements(Elements::LeftRotate64), - Self::Elements(Elements::LeftRotate8), - Self::Elements(Elements::LeftShift16), - Self::Elements(Elements::LeftShift32), - Self::Elements(Elements::LeftShift64), - Self::Elements(Elements::LeftShift8), - Self::Elements(Elements::LeftShiftWith16), - Self::Elements(Elements::LeftShiftWith32), - Self::Elements(Elements::LeftShiftWith64), - Self::Elements(Elements::LeftShiftWith8), - Self::Elements(Elements::Leftmost16_1), - Self::Elements(Elements::Leftmost16_2), - Self::Elements(Elements::Leftmost16_4), - Self::Elements(Elements::Leftmost16_8), - Self::Elements(Elements::Leftmost32_1), - Self::Elements(Elements::Leftmost32_16), - Self::Elements(Elements::Leftmost32_2), - Self::Elements(Elements::Leftmost32_4), - Self::Elements(Elements::Leftmost32_8), - Self::Elements(Elements::Leftmost64_1), - Self::Elements(Elements::Leftmost64_16), - Self::Elements(Elements::Leftmost64_2), - Self::Elements(Elements::Leftmost64_32), - Self::Elements(Elements::Leftmost64_4), - Self::Elements(Elements::Leftmost64_8), - Self::Elements(Elements::Leftmost8_1), - Self::Elements(Elements::Leftmost8_2), - Self::Elements(Elements::Leftmost8_4), - Self::Elements(Elements::LinearCombination1), - Self::Elements(Elements::LinearVerify1), - Self::Elements(Elements::LockTime), - Self::Elements(Elements::Low1), - Self::Elements(Elements::Low16), - Self::Elements(Elements::Low32), - Self::Elements(Elements::Low64), - Self::Elements(Elements::Low8), - Self::Elements(Elements::Lt16), - Self::Elements(Elements::Lt32), - Self::Elements(Elements::Lt64), - Self::Elements(Elements::Lt8), - Self::Elements(Elements::Maj1), - Self::Elements(Elements::Maj16), - Self::Elements(Elements::Maj32), - Self::Elements(Elements::Maj64), - Self::Elements(Elements::Maj8), - Self::Elements(Elements::Max16), - Self::Elements(Elements::Max32), - Self::Elements(Elements::Max64), - Self::Elements(Elements::Max8), - Self::Elements(Elements::Median16), - Self::Elements(Elements::Median32), - Self::Elements(Elements::Median64), - Self::Elements(Elements::Median8), - Self::Elements(Elements::Min16), - Self::Elements(Elements::Min32), - Self::Elements(Elements::Min64), - Self::Elements(Elements::Min8), - Self::Elements(Elements::Modulo16), - Self::Elements(Elements::Modulo32), - Self::Elements(Elements::Modulo64), - Self::Elements(Elements::Modulo8), - Self::Elements(Elements::Multiply16), - Self::Elements(Elements::Multiply32), - Self::Elements(Elements::Multiply64), - Self::Elements(Elements::Multiply8), - Self::Elements(Elements::Negate16), - Self::Elements(Elements::Negate32), - Self::Elements(Elements::Negate64), - Self::Elements(Elements::Negate8), - Self::Elements(Elements::NewIssuanceContract), - Self::Elements(Elements::NonceHash), - Self::Elements(Elements::NumInputs), - Self::Elements(Elements::NumOutputs), - Self::Elements(Elements::One16), - Self::Elements(Elements::One32), - Self::Elements(Elements::One64), - Self::Elements(Elements::One8), - Self::Elements(Elements::Or1), - Self::Elements(Elements::Or16), - Self::Elements(Elements::Or32), - Self::Elements(Elements::Or64), - Self::Elements(Elements::Or8), - Self::Elements(Elements::OutpointHash), - Self::Elements(Elements::OutputAmount), - Self::Elements(Elements::OutputAmountsHash), - Self::Elements(Elements::OutputAsset), - Self::Elements(Elements::OutputHash), - Self::Elements(Elements::OutputIsFee), - Self::Elements(Elements::OutputNonce), - Self::Elements(Elements::OutputNoncesHash), - Self::Elements(Elements::OutputNullDatum), - Self::Elements(Elements::OutputRangeProof), - Self::Elements(Elements::OutputRangeProofsHash), - Self::Elements(Elements::OutputScriptHash), - Self::Elements(Elements::OutputScriptsHash), - Self::Elements(Elements::OutputSurjectionProof), - Self::Elements(Elements::OutputSurjectionProofsHash), - Self::Elements(Elements::OutputsHash), - Self::Elements(Elements::ParseLock), - Self::Elements(Elements::ParseSequence), - Self::Elements(Elements::PointVerify1), - Self::Elements(Elements::ReissuanceBlinding), - Self::Elements(Elements::ReissuanceEntropy), - Self::Elements(Elements::RightExtend16_32), - Self::Elements(Elements::RightExtend16_64), - Self::Elements(Elements::RightExtend32_64), - Self::Elements(Elements::RightExtend8_16), - Self::Elements(Elements::RightExtend8_32), - Self::Elements(Elements::RightExtend8_64), - Self::Elements(Elements::RightPadHigh16_32), - Self::Elements(Elements::RightPadHigh16_64), - Self::Elements(Elements::RightPadHigh1_16), - Self::Elements(Elements::RightPadHigh1_32), - Self::Elements(Elements::RightPadHigh1_64), - Self::Elements(Elements::RightPadHigh1_8), - Self::Elements(Elements::RightPadHigh32_64), - Self::Elements(Elements::RightPadHigh8_16), - Self::Elements(Elements::RightPadHigh8_32), - Self::Elements(Elements::RightPadHigh8_64), - Self::Elements(Elements::RightPadLow16_32), - Self::Elements(Elements::RightPadLow16_64), - Self::Elements(Elements::RightPadLow1_16), - Self::Elements(Elements::RightPadLow1_32), - Self::Elements(Elements::RightPadLow1_64), - Self::Elements(Elements::RightPadLow1_8), - Self::Elements(Elements::RightPadLow32_64), - Self::Elements(Elements::RightPadLow8_16), - Self::Elements(Elements::RightPadLow8_32), - Self::Elements(Elements::RightPadLow8_64), - Self::Elements(Elements::RightRotate16), - Self::Elements(Elements::RightRotate32), - Self::Elements(Elements::RightRotate64), - Self::Elements(Elements::RightRotate8), - Self::Elements(Elements::RightShift16), - Self::Elements(Elements::RightShift32), - Self::Elements(Elements::RightShift64), - Self::Elements(Elements::RightShift8), - Self::Elements(Elements::RightShiftWith16), - Self::Elements(Elements::RightShiftWith32), - Self::Elements(Elements::RightShiftWith64), - Self::Elements(Elements::RightShiftWith8), - Self::Elements(Elements::Rightmost16_1), - Self::Elements(Elements::Rightmost16_2), - Self::Elements(Elements::Rightmost16_4), - Self::Elements(Elements::Rightmost16_8), - Self::Elements(Elements::Rightmost32_1), - Self::Elements(Elements::Rightmost32_16), - Self::Elements(Elements::Rightmost32_2), - Self::Elements(Elements::Rightmost32_4), - Self::Elements(Elements::Rightmost32_8), - Self::Elements(Elements::Rightmost64_1), - Self::Elements(Elements::Rightmost64_16), - Self::Elements(Elements::Rightmost64_2), - Self::Elements(Elements::Rightmost64_32), - Self::Elements(Elements::Rightmost64_4), - Self::Elements(Elements::Rightmost64_8), - Self::Elements(Elements::Rightmost8_1), - Self::Elements(Elements::Rightmost8_2), - Self::Elements(Elements::Rightmost8_4), - Self::Elements(Elements::ScalarAdd), - Self::Elements(Elements::ScalarInvert), - Self::Elements(Elements::ScalarIsZero), - Self::Elements(Elements::ScalarMultiply), - Self::Elements(Elements::ScalarMultiplyLambda), - Self::Elements(Elements::ScalarNegate), - Self::Elements(Elements::ScalarNormalize), - Self::Elements(Elements::ScalarSquare), - Self::Elements(Elements::Scale), - Self::Elements(Elements::ScriptCMR), - Self::Elements(Elements::Sha256Block), - Self::Elements(Elements::Sha256Ctx8Add1), - Self::Elements(Elements::Sha256Ctx8Add128), - Self::Elements(Elements::Sha256Ctx8Add16), - Self::Elements(Elements::Sha256Ctx8Add2), - Self::Elements(Elements::Sha256Ctx8Add256), - Self::Elements(Elements::Sha256Ctx8Add32), - Self::Elements(Elements::Sha256Ctx8Add4), - Self::Elements(Elements::Sha256Ctx8Add512), - Self::Elements(Elements::Sha256Ctx8Add64), - Self::Elements(Elements::Sha256Ctx8Add8), - Self::Elements(Elements::Sha256Ctx8AddBuffer511), - Self::Elements(Elements::Sha256Ctx8Finalize), - Self::Elements(Elements::Sha256Ctx8Init), - Self::Elements(Elements::Sha256Iv), - Self::Elements(Elements::SigAllHash), - Self::Elements(Elements::Some1), - Self::Elements(Elements::Some16), - Self::Elements(Elements::Some32), - Self::Elements(Elements::Some64), - Self::Elements(Elements::Some8), - Self::Elements(Elements::Subtract16), - Self::Elements(Elements::Subtract32), - Self::Elements(Elements::Subtract64), - Self::Elements(Elements::Subtract8), - Self::Elements(Elements::Swu), - Self::Elements(Elements::TapEnvHash), - Self::Elements(Elements::TapdataInit), - Self::Elements(Elements::TapleafHash), - Self::Elements(Elements::TapleafVersion), - Self::Elements(Elements::Tappath), - Self::Elements(Elements::TappathHash), - Self::Elements(Elements::TotalFee), - Self::Elements(Elements::TransactionId), - Self::Elements(Elements::TxHash), - Self::Elements(Elements::TxIsFinal), - Self::Elements(Elements::TxLockDistance), - Self::Elements(Elements::TxLockDuration), - Self::Elements(Elements::TxLockHeight), - Self::Elements(Elements::TxLockTime), - Self::Elements(Elements::Verify), - Self::Elements(Elements::Version), - Self::Elements(Elements::Xor1), - Self::Elements(Elements::Xor16), - Self::Elements(Elements::Xor32), - Self::Elements(Elements::Xor64), - Self::Elements(Elements::Xor8), - Self::Elements(Elements::XorXor1), - Self::Elements(Elements::XorXor16), - Self::Elements(Elements::XorXor32), - Self::Elements(Elements::XorXor64), - Self::Elements(Elements::XorXor8), - Self::GetOpcodeFromScript, - Self::GetPubkeyFromScript, - ]; + pub const ALL: [Self; Self::ALL_JETS_NUM] = Self::build_all_variants(); + + const ALL_JETS_NUM: usize = Elements::ALL.len() + 2; + + const fn build_all_variants() -> [Self; Self::ALL_JETS_NUM] { + struct AllVariantsBuilder { + data: [MaybeUninit; ElementsExtension::ALL_JETS_NUM], + len: usize, + } + + impl AllVariantsBuilder { + const fn new() -> Self { + Self { + data: [MaybeUninit::uninit(); ElementsExtension::ALL_JETS_NUM], + len: 0, + } + } + + const fn push(&mut self, item: ElementsExtension) { + assert!(self.len < self.data.len()); + + self.data[self.len].write(item); + self.len += 1; + } + + const fn finalize(self) -> [ElementsExtension; ElementsExtension::ALL_JETS_NUM] { + assert!(self.len == ElementsExtension::ALL_JETS_NUM); + + unsafe { std::mem::transmute(self.data) } + } + } + + let mut builder = AllVariantsBuilder::new(); + let mut i = 0; + + while i < Elements::ALL.len() { + builder.push(ElementsExtension::Elements(Elements::ALL[i])); + i += 1; + } + + builder.push(ElementsExtension::GetOpcodeFromScript); + builder.push(ElementsExtension::GetPubkeyFromScript); + + builder.finalize() + } } -impl Jet for ElementsExtension { - type Environment = UnchainedEnv>>; - type CJetEnvironment = UnchainedEnv>>; +impl JetEnvironment for ElementsUnchainedEnv { + type Jet = ElementsExtension; + type CJetEnvironment = Self; - fn c_jet_env(env: &Self::Environment) -> &Self::CJetEnvironment { + fn c_jet_env(&self) -> &Self::CJetEnvironment { // For the time being, we are goint to use the initial environment for unchained jets, // as we are going to implement them in rust. - env + self } + fn c_jet_ptr( + jet: &Self::Jet, + ) -> fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool { + match jet { + // hijacked elements jets + ElementsExtension::Elements(Elements::BrokenDoNotUseCheckLockDuration) => { + super::exec::check_lock_duration + } + ElementsExtension::Elements(Elements::BrokenDoNotUseCheckLockDistance) => { + super::exec::check_lock_distance + } + ElementsExtension::Elements(Elements::BrokenDoNotUseTxLockDuration) => { + super::exec::tx_lock_duration + } + ElementsExtension::Elements(Elements::BrokenDoNotUseTxLockDistance) => { + super::exec::tx_lock_distance + } + // rest of elements jets + ElementsExtension::Elements(inner_jet) => base_c_jet_ptr(inner_jet), + // custom jets + ElementsExtension::GetOpcodeFromScript => { + super::exec::get_opcode_from_script:: + } + ElementsExtension::GetPubkeyFromScript => { + super::exec::get_pubkey_from_script:: + } + } + } +} + +impl Jet for ElementsExtension { fn cmr(&self) -> Cmr { if let ElementsExtension::Elements(inner_jet) = self { return inner_jet.cmr(); @@ -575,7 +174,7 @@ impl Jet for ElementsExtension { TypeName(name) } - fn encode(&self, w: &mut BitWriter) -> std::io::Result { + fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { if let ElementsExtension::Elements(inner_jet) = self { return inner_jet.encode(w); } @@ -589,2655 +188,42 @@ impl Jet for ElementsExtension { w.write_bits_be(n, len) } + /// # Safety + /// + /// Due to the lack of a `Clone` bound on `I`, the underlying implementation uses + /// `ptr::read` to create bitwise copies of the iterator. This is unsafe and may cause + /// undefined behavior if `I` contains types that manage unique resources. + /// This works correctly for common slice-based iterators like `Copied>`. + /// + /// See for details. fn decode>(bits: &mut BitIter) -> Result { - decode_bits!(bits, { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Verify)}, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Low1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Low8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Low16)}, - 1 => {ElementsExtension::Elements(Elements::Low32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Low64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::High1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::High8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::High16)}, - 1 => {ElementsExtension::Elements(Elements::High32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::High64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Complement1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Complement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Complement16)}, - 1 => {ElementsExtension::Elements(Elements::Complement32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Complement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::And1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::And8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::And16)}, - 1 => {ElementsExtension::Elements(Elements::And32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::And64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Or1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Or8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Or16)}, - 1 => {ElementsExtension::Elements(Elements::Or32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Or64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Xor1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Xor8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Xor16)}, - 1 => {ElementsExtension::Elements(Elements::Xor32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Xor64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Maj1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Maj8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Maj16)}, - 1 => {ElementsExtension::Elements(Elements::Maj32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Maj64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::XorXor1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::XorXor8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::XorXor16)}, - 1 => {ElementsExtension::Elements(Elements::XorXor32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::XorXor64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Ch1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Ch8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Ch16)}, - 1 => {ElementsExtension::Elements(Elements::Ch32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Ch64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Some1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Some8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Some16)}, - 1 => {ElementsExtension::Elements(Elements::Some32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Some64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::All8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::All16)}, - 1 => {ElementsExtension::Elements(Elements::All32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::All64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Eq1)}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Eq8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Eq16)}, - 1 => {ElementsExtension::Elements(Elements::Eq32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Eq64)}, - 1 => {} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Eq256)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FullLeftShift8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift16_1)}, - 1 => {ElementsExtension::Elements(Elements::FullLeftShift32_1)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift8_2)}, - 1 => {ElementsExtension::Elements(Elements::FullLeftShift16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift32_2)}, - 1 => {ElementsExtension::Elements(Elements::FullLeftShift64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift8_4)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift16_4)}, - 1 => {ElementsExtension::Elements(Elements::FullLeftShift32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift16_8)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift32_8)}, - 1 => {ElementsExtension::Elements(Elements::FullLeftShift64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift32_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullLeftShift64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FullRightShift8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift16_1)}, - 1 => {ElementsExtension::Elements(Elements::FullRightShift32_1)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift8_2)}, - 1 => {ElementsExtension::Elements(Elements::FullRightShift16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift32_2)}, - 1 => {ElementsExtension::Elements(Elements::FullRightShift64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift8_4)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift16_4)}, - 1 => {ElementsExtension::Elements(Elements::FullRightShift32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift16_8)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift32_8)}, - 1 => {ElementsExtension::Elements(Elements::FullRightShift64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift32_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullRightShift64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Leftmost8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost16_1)}, - 1 => {ElementsExtension::Elements(Elements::Leftmost32_1)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost8_2)}, - 1 => {ElementsExtension::Elements(Elements::Leftmost16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost32_2)}, - 1 => {ElementsExtension::Elements(Elements::Leftmost64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost8_4)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost16_4)}, - 1 => {ElementsExtension::Elements(Elements::Leftmost32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost16_8)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost32_8)}, - 1 => {ElementsExtension::Elements(Elements::Leftmost64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost32_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Leftmost64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Rightmost8_1)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost16_1)}, - 1 => {ElementsExtension::Elements(Elements::Rightmost32_1)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost64_1)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost8_2)}, - 1 => {ElementsExtension::Elements(Elements::Rightmost16_2)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost32_2)}, - 1 => {ElementsExtension::Elements(Elements::Rightmost64_2)} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost8_4)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost16_4)}, - 1 => {ElementsExtension::Elements(Elements::Rightmost32_4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost64_4)}, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost16_8)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost32_8)}, - 1 => {ElementsExtension::Elements(Elements::Rightmost64_8)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost32_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost64_16)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Rightmost64_32)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::LeftPadLow1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadLow1_16)}, - 1 => {ElementsExtension::Elements(Elements::LeftPadLow1_32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadLow1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadLow8_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadLow8_32)}, - 1 => {ElementsExtension::Elements(Elements::LeftPadLow8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadLow16_32)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadLow16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadLow32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::LeftPadHigh1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadHigh1_16)}, - 1 => {ElementsExtension::Elements(Elements::LeftPadHigh1_32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadHigh1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadHigh8_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadHigh8_32)}, - 1 => {ElementsExtension::Elements(Elements::LeftPadHigh8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadHigh16_32)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadHigh16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftPadHigh32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::LeftExtend1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftExtend1_16)}, - 1 => {ElementsExtension::Elements(Elements::LeftExtend1_32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftExtend1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftExtend8_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftExtend8_32)}, - 1 => {ElementsExtension::Elements(Elements::LeftExtend8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftExtend16_32)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftExtend16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftExtend32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::RightPadLow1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadLow1_16)}, - 1 => {ElementsExtension::Elements(Elements::RightPadLow1_32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightPadLow1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadLow8_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadLow8_32)}, - 1 => {ElementsExtension::Elements(Elements::RightPadLow8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightPadLow16_32)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadLow16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadLow32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::RightPadHigh1_8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadHigh1_16)}, - 1 => {ElementsExtension::Elements(Elements::RightPadHigh1_32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightPadHigh1_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadHigh8_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadHigh8_32)}, - 1 => {ElementsExtension::Elements(Elements::RightPadHigh8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightPadHigh16_32)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadHigh16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightPadHigh32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightExtend8_16)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightExtend8_32)}, - 1 => {ElementsExtension::Elements(Elements::RightExtend8_64)} - }, - 1 => {} - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightExtend16_32)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightExtend16_64)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightExtend32_64)}, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::LeftShiftWith8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftShiftWith16)}, - 1 => {ElementsExtension::Elements(Elements::LeftShiftWith32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftShiftWith64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::RightShiftWith8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightShiftWith16)}, - 1 => {ElementsExtension::Elements(Elements::RightShiftWith32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightShiftWith64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::LeftShift8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftShift16)}, - 1 => {ElementsExtension::Elements(Elements::LeftShift32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftShift64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::RightShift8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightShift16)}, - 1 => {ElementsExtension::Elements(Elements::RightShift32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightShift64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::LeftRotate8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LeftRotate16)}, - 1 => {ElementsExtension::Elements(Elements::LeftRotate32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LeftRotate64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::RightRotate8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::RightRotate16)}, - 1 => {ElementsExtension::Elements(Elements::RightRotate32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::RightRotate64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => {} - } - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::One8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::One16)}, - 1 => {ElementsExtension::Elements(Elements::One32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::One64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FullAdd8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullAdd16)}, - 1 => {ElementsExtension::Elements(Elements::FullAdd32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullAdd64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Add8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Add16)}, - 1 => {ElementsExtension::Elements(Elements::Add32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Add64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FullIncrement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullIncrement16)}, - 1 => {ElementsExtension::Elements(Elements::FullIncrement32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullIncrement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Increment8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Increment16)}, - 1 => {ElementsExtension::Elements(Elements::Increment32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Increment64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FullSubtract8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullSubtract16)}, - 1 => {ElementsExtension::Elements(Elements::FullSubtract32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullSubtract64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Subtract8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Subtract16)}, - 1 => {ElementsExtension::Elements(Elements::Subtract32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Subtract64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Negate8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Negate16)}, - 1 => {ElementsExtension::Elements(Elements::Negate32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Negate64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FullDecrement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullDecrement16)}, - 1 => {ElementsExtension::Elements(Elements::FullDecrement32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullDecrement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Decrement8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Decrement16)}, - 1 => {ElementsExtension::Elements(Elements::Decrement32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Decrement64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FullMultiply8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FullMultiply16)}, - 1 => {ElementsExtension::Elements(Elements::FullMultiply32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FullMultiply64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Multiply8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Multiply16)}, - 1 => {ElementsExtension::Elements(Elements::Multiply32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Multiply64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::IsZero8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::IsZero16)}, - 1 => {ElementsExtension::Elements(Elements::IsZero32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::IsZero64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::IsOne8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::IsOne16)}, - 1 => {ElementsExtension::Elements(Elements::IsOne32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::IsOne64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Le8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Le16)}, - 1 => {ElementsExtension::Elements(Elements::Le32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Le64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Lt8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Lt16)}, - 1 => {ElementsExtension::Elements(Elements::Lt32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Lt64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Min8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Min16)}, - 1 => {ElementsExtension::Elements(Elements::Min32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Min64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Max8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Max16)}, - 1 => {ElementsExtension::Elements(Elements::Max32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Max64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Median8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Median16)}, - 1 => {ElementsExtension::Elements(Elements::Median32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Median64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => {ElementsExtension::Elements(Elements::DivMod128_64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::DivMod8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::DivMod16)}, - 1 => {ElementsExtension::Elements(Elements::DivMod32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::DivMod64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Divide8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Divide16)}, - 1 => {ElementsExtension::Elements(Elements::Divide32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Divide64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Modulo8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Modulo16)}, - 1 => {ElementsExtension::Elements(Elements::Modulo32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Modulo64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::Divides8)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Divides16)}, - 1 => {ElementsExtension::Elements(Elements::Divides32)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Divides64)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Block)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Iv)}, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add1)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add2)}, - 1 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add4)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add8)}, - 1 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add16)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add32)}, - 1 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add64)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add128)}, - 1 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add256)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8Add512)}, - 1 => {} - } - }, - 1 => {} - } - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8AddBuffer511)}, - 1 => {ElementsExtension::Elements(Elements::Sha256Ctx8Finalize)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Sha256Ctx8Init)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => {} - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::PointVerify1)}, - 1 => {} - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Decompress)}, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LinearVerify1)}, - 1 => {} - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::LinearCombination1)}, - 1 => {} - }, - 1 => {ElementsExtension::Elements(Elements::Scale)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Generate)}, - 1 => {ElementsExtension::Elements(Elements::GejInfinity)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::GejNormalize)}, - 1 => {ElementsExtension::Elements(Elements::GejNegate)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::GeNegate)}, - 1 => {ElementsExtension::Elements(Elements::GejDouble)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::GejAdd)}, - 1 => {ElementsExtension::Elements(Elements::GejGeAddEx)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::GejGeAdd)}, - 1 => {ElementsExtension::Elements(Elements::GejRescale)} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::GejIsInfinity)}, - 1 => {ElementsExtension::Elements(Elements::GejEquiv)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::GejGeEquiv)}, - 1 => {ElementsExtension::Elements(Elements::GejXEquiv)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::GejYIsOdd)}, - 1 => {ElementsExtension::Elements(Elements::GejIsOnCurve)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::GeIsOnCurve)}, - 1 => {ElementsExtension::Elements(Elements::ScalarNormalize)} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::ScalarNegate)}, - 1 => {ElementsExtension::Elements(Elements::ScalarAdd)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::ScalarSquare)}, - 1 => {ElementsExtension::Elements(Elements::ScalarMultiply)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::ScalarMultiplyLambda)}, - 1 => {ElementsExtension::Elements(Elements::ScalarInvert)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::ScalarIsZero)}, - 1 => {} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {}, - 1 => { - 0 => {}, - 1 => {ElementsExtension::Elements(Elements::FeNormalize)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FeNegate)}, - 1 => {ElementsExtension::Elements(Elements::FeAdd)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FeSquare)}, - 1 => {ElementsExtension::Elements(Elements::FeMultiply)} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FeMultiplyBeta)}, - 1 => {ElementsExtension::Elements(Elements::FeInvert)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::FeSquareRoot)}, - 1 => {ElementsExtension::Elements(Elements::FeIsZero)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::FeIsOdd)}, - 1 => {} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::HashToCurve)}, - 1 => {ElementsExtension::Elements(Elements::Swu)} - } - } - } - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::CheckSigVerify)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Bip0340Verify)}, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => { - 0 => {}, - 1 => { - 0 => {ElementsExtension::Elements(Elements::ParseLock)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::ParseSequence)}, - 1 => {ElementsExtension::Elements(Elements::TapdataInit)} - }, - 1 => {} - } - } - } - }, - 1 => {} - }, - 1 => {} - } - } - }, + let (mut elements_iter, mut custom_iter) = + unsafe { (std::ptr::read(bits), std::ptr::read(bits)) }; + + let bits_read = bits.n_total_read(); + + let try_elements = Elements::decode(&mut elements_iter); + + if let Ok(jet) = try_elements { + for _ in 0..(elements_iter.n_total_read() - bits_read) { + bits.next(); + } + + std::mem::forget(elements_iter); + std::mem::forget(custom_iter); + + return Ok(ElementsExtension::Elements(jet)); + } + + let custom_iter_ref = &mut custom_iter; + let try_custom = decode_bits!(custom_iter_ref, { + 0 => {}, 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::SigAllHash)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::TxHash)}, - 1 => {ElementsExtension::Elements(Elements::TapEnvHash)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::OutputsHash)}, - 1 => {ElementsExtension::Elements(Elements::InputsHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::IssuancesHash)}, - 1 => {ElementsExtension::Elements(Elements::InputUtxosHash)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::OutputHash)}, - 1 => {ElementsExtension::Elements(Elements::OutputAmountsHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::OutputScriptsHash)}, - 1 => {ElementsExtension::Elements(Elements::OutputNoncesHash)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::OutputRangeProofsHash)}, - 1 => {ElementsExtension::Elements(Elements::OutputSurjectionProofsHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::InputHash)}, - 1 => {ElementsExtension::Elements(Elements::InputOutpointsHash)} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::InputSequencesHash)}, - 1 => {ElementsExtension::Elements(Elements::InputAnnexesHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::InputScriptSigsHash)}, - 1 => {ElementsExtension::Elements(Elements::IssuanceHash)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::IssuanceAssetAmountsHash)}, - 1 => {ElementsExtension::Elements(Elements::IssuanceTokenAmountsHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::IssuanceRangeProofsHash)}, - 1 => {ElementsExtension::Elements(Elements::IssuanceBlindingEntropyHash)} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::InputUtxoHash)}, - 1 => {ElementsExtension::Elements(Elements::InputAmountsHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::InputScriptsHash)}, - 1 => {ElementsExtension::Elements(Elements::TapleafHash)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::TappathHash)}, - 1 => {ElementsExtension::Elements(Elements::OutpointHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::AssetAmountHash)}, - 1 => {ElementsExtension::Elements(Elements::NonceHash)} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::AnnexHash)}, - 1 => {ElementsExtension::Elements(Elements::BuildTapleafSimplicity)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::BuildTapbranch)}, - 1 => {ElementsExtension::Elements(Elements::BuildTaptweak)} - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, + 0 => {}, 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CheckLockHeight)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CheckLockTime)}, - 1 => {ElementsExtension::Elements(Elements::CheckLockDistance)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CheckLockDuration)}, - 1 => {ElementsExtension::Elements(Elements::TxLockHeight)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::TxLockTime)}, - 1 => {ElementsExtension::Elements(Elements::TxLockDistance)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::TxLockDuration)}, - 1 => {ElementsExtension::Elements(Elements::TxIsFinal)} - }, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - } - } - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::Issuance)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::IssuanceAsset)}, - 1 => {ElementsExtension::Elements(Elements::IssuanceToken)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::IssuanceEntropy)}, - 1 => {ElementsExtension::Elements(Elements::CalculateIssuanceEntropy)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::CalculateAsset)}, - 1 => {ElementsExtension::Elements(Elements::CalculateExplicitToken)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CalculateConfidentialToken)}, - 1 => {ElementsExtension::Elements(Elements::LbtcAsset)} - }, - 1 => {} - }, - 1 => {} - } - }, - 1 => {} - } - } - } - }, + 0 => {}, 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::ScriptCMR)}, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::InternalKey)}, - 1 => {ElementsExtension::Elements(Elements::CurrentIndex)} - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::NumInputs)}, - 1 => {ElementsExtension::Elements(Elements::NumOutputs)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::LockTime)}, - 1 => {ElementsExtension::Elements(Elements::OutputAsset)} - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::OutputAmount)}, - 1 => {ElementsExtension::Elements(Elements::OutputNonce)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::OutputScriptHash)}, - 1 => {ElementsExtension::Elements(Elements::OutputNullDatum)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::OutputIsFee)}, - 1 => {ElementsExtension::Elements(Elements::OutputSurjectionProof)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::OutputRangeProof)}, - 1 => {ElementsExtension::Elements(Elements::TotalFee)} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CurrentPegin)}, - 1 => {ElementsExtension::Elements(Elements::CurrentPrevOutpoint)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::CurrentAsset)}, - 1 => {ElementsExtension::Elements(Elements::CurrentAmount)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CurrentScriptHash)}, - 1 => {ElementsExtension::Elements(Elements::CurrentSequence)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::CurrentAnnexHash)}, - 1 => {ElementsExtension::Elements(Elements::CurrentScriptSigHash)} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CurrentReissuanceBlinding)}, - 1 => {ElementsExtension::Elements(Elements::CurrentNewIssuanceContract)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::CurrentReissuanceEntropy)}, - 1 => {ElementsExtension::Elements(Elements::CurrentIssuanceAssetAmount)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::CurrentIssuanceTokenAmount)}, - 1 => {ElementsExtension::Elements(Elements::CurrentIssuanceAssetProof)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::CurrentIssuanceTokenProof)}, - 1 => {ElementsExtension::Elements(Elements::InputPegin)} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::InputPrevOutpoint)}, - 1 => {ElementsExtension::Elements(Elements::InputAsset)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::InputAmount)}, - 1 => {ElementsExtension::Elements(Elements::InputScriptHash)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::InputSequence)}, - 1 => {ElementsExtension::Elements(Elements::InputAnnexHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::InputScriptSigHash)}, - 1 => {ElementsExtension::Elements(Elements::ReissuanceBlinding)} - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::NewIssuanceContract)}, - 1 => {ElementsExtension::Elements(Elements::ReissuanceEntropy)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::IssuanceAssetAmount)}, - 1 => {ElementsExtension::Elements(Elements::IssuanceTokenAmount)} - } - }, - 1 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::IssuanceAssetProof)}, - 1 => {ElementsExtension::Elements(Elements::IssuanceTokenProof)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::TapleafVersion)}, - 1 => {ElementsExtension::Elements(Elements::Tappath)} - } - } - } - }, - 1 => { - 0 => { - 0 => { - 0 => { - 0 => {ElementsExtension::Elements(Elements::Version)}, - 1 => {ElementsExtension::Elements(Elements::GenesisBlockHash)} - }, - 1 => { - 0 => {ElementsExtension::Elements(Elements::TransactionId)}, - 1 => {} - } - }, - 1 => {} - }, - 1 => {} - } - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - } - } - } - }, - 1 => {} - }, - 1 => {} - }, - 1 => {} - }, + 0 => {}, 1 => { 0 => {}, // Free path 1 => { @@ -3251,23 +237,18 @@ impl Jet for ElementsExtension { } } } - }) - } + }); - fn c_jet_ptr(&self) -> &dyn Fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool { - match self { - ElementsExtension::Elements(Elements::CheckLockDuration) => { - &super::exec::check_lock_duration - } - ElementsExtension::Elements(Elements::CheckLockDistance) => { - &super::exec::check_lock_distance + if try_custom.is_ok() { + for _ in 0..(custom_iter.n_total_read() - bits_read) { + bits.next(); } - ElementsExtension::Elements(Elements::TxLockDuration) => &super::exec::tx_lock_duration, - ElementsExtension::Elements(Elements::TxLockDistance) => &super::exec::tx_lock_distance, - ElementsExtension::Elements(inner_jet) => jet_wrapper(*inner_jet), - ElementsExtension::GetOpcodeFromScript => &super::exec::get_opcode_from_script, - ElementsExtension::GetPubkeyFromScript => &super::exec::get_pubkey_from_script, } + + std::mem::forget(elements_iter); + std::mem::forget(custom_iter); + + try_custom } fn cost(&self) -> Cost { @@ -3282,6 +263,10 @@ impl Jet for ElementsExtension { _ => unreachable!(), } } + + fn parse(s: &str) -> Result { + s.parse() + } } impl std::fmt::Display for ElementsExtension { @@ -3295,7 +280,7 @@ impl std::fmt::Display for ElementsExtension { } impl std::str::FromStr for ElementsExtension { - type Err = hal_simplicity::simplicity::Error; + type Err = simplicity::Error; fn from_str(s: &str) -> Result { match s { @@ -3308,507 +293,3 @@ impl std::str::FromStr for ElementsExtension { } } } - -// Macro to generate static wrapper functions AND dispatcher for Elements jets -// This macro generates both the wrapper functions and the match statement in one go, -// so we only need to list each Elements variant once. -macro_rules! jet_wrappers { - ($($variant:ident),* $(,)?) => { - // Generate individual wrapper functions for each variant - $( - #[allow(non_snake_case)] - fn $variant(frame: &mut CFrameItem, arg: CFrameItem, env: &UnchainedEnv>>) -> bool { - Elements::$variant.c_jet_ptr()(frame, arg, env.env.c_tx_env()) - } - )* - - // Generate the dispatcher function that returns the appropriate wrapper - fn jet_wrapper(jet: Elements) -> &'static dyn Fn(&mut CFrameItem, CFrameItem, &UnchainedEnv>>) -> bool { - match jet { - $( - Elements::$variant => &$variant, - )* - } - } - }; -} - -// Generate wrapper functions and dispatcher for all Elements jet variants -// If Elements enum changes, only update this list to keep wrappers in sync -// -// TODO(ivanlele): This is extremly ungly solution, will need to open an issue -// for `rust-simplicity` to better interface for jet trait that -// does not require this boilerplate -jet_wrappers! { - Add16, - Add32, - Add64, - Add8, - All16, - All32, - All64, - All8, - And1, - And16, - And32, - And64, - And8, - AnnexHash, - AssetAmountHash, - Bip0340Verify, - BuildTapbranch, - BuildTapleafSimplicity, - BuildTaptweak, - CalculateAsset, - CalculateConfidentialToken, - CalculateExplicitToken, - CalculateIssuanceEntropy, - Ch1, - Ch16, - Ch32, - Ch64, - Ch8, - CheckLockDistance, - CheckLockDuration, - CheckLockHeight, - CheckLockTime, - CheckSigVerify, - Complement1, - Complement16, - Complement32, - Complement64, - Complement8, - CurrentAmount, - CurrentAnnexHash, - CurrentAsset, - CurrentIndex, - CurrentIssuanceAssetAmount, - CurrentIssuanceAssetProof, - CurrentIssuanceTokenAmount, - CurrentIssuanceTokenProof, - CurrentNewIssuanceContract, - CurrentPegin, - CurrentPrevOutpoint, - CurrentReissuanceBlinding, - CurrentReissuanceEntropy, - CurrentScriptHash, - CurrentScriptSigHash, - CurrentSequence, - Decompress, - Decrement16, - Decrement32, - Decrement64, - Decrement8, - DivMod128_64, - DivMod16, - DivMod32, - DivMod64, - DivMod8, - Divide16, - Divide32, - Divide64, - Divide8, - Divides16, - Divides32, - Divides64, - Divides8, - Eq1, - Eq16, - Eq256, - Eq32, - Eq64, - Eq8, - FeAdd, - FeInvert, - FeIsOdd, - FeIsZero, - FeMultiply, - FeMultiplyBeta, - FeNegate, - FeNormalize, - FeSquare, - FeSquareRoot, - FullAdd16, - FullAdd32, - FullAdd64, - FullAdd8, - FullDecrement16, - FullDecrement32, - FullDecrement64, - FullDecrement8, - FullIncrement16, - FullIncrement32, - FullIncrement64, - FullIncrement8, - FullLeftShift16_1, - FullLeftShift16_2, - FullLeftShift16_4, - FullLeftShift16_8, - FullLeftShift32_1, - FullLeftShift32_16, - FullLeftShift32_2, - FullLeftShift32_4, - FullLeftShift32_8, - FullLeftShift64_1, - FullLeftShift64_16, - FullLeftShift64_2, - FullLeftShift64_32, - FullLeftShift64_4, - FullLeftShift64_8, - FullLeftShift8_1, - FullLeftShift8_2, - FullLeftShift8_4, - FullMultiply16, - FullMultiply32, - FullMultiply64, - FullMultiply8, - FullRightShift16_1, - FullRightShift16_2, - FullRightShift16_4, - FullRightShift16_8, - FullRightShift32_1, - FullRightShift32_16, - FullRightShift32_2, - FullRightShift32_4, - FullRightShift32_8, - FullRightShift64_1, - FullRightShift64_16, - FullRightShift64_2, - FullRightShift64_32, - FullRightShift64_4, - FullRightShift64_8, - FullRightShift8_1, - FullRightShift8_2, - FullRightShift8_4, - FullSubtract16, - FullSubtract32, - FullSubtract64, - FullSubtract8, - GeIsOnCurve, - GeNegate, - GejAdd, - GejDouble, - GejEquiv, - GejGeAdd, - GejGeAddEx, - GejGeEquiv, - GejInfinity, - GejIsInfinity, - GejIsOnCurve, - GejNegate, - GejNormalize, - GejRescale, - GejXEquiv, - GejYIsOdd, - Generate, - GenesisBlockHash, - HashToCurve, - High1, - High16, - High32, - High64, - High8, - Increment16, - Increment32, - Increment64, - Increment8, - InputAmount, - InputAmountsHash, - InputAnnexHash, - InputAnnexesHash, - InputAsset, - InputHash, - InputOutpointsHash, - InputPegin, - InputPrevOutpoint, - InputScriptHash, - InputScriptSigHash, - InputScriptSigsHash, - InputScriptsHash, - InputSequence, - InputSequencesHash, - InputUtxoHash, - InputUtxosHash, - InputsHash, - InternalKey, - IsOne16, - IsOne32, - IsOne64, - IsOne8, - IsZero16, - IsZero32, - IsZero64, - IsZero8, - Issuance, - IssuanceAsset, - IssuanceAssetAmount, - IssuanceAssetAmountsHash, - IssuanceAssetProof, - IssuanceBlindingEntropyHash, - IssuanceEntropy, - IssuanceHash, - IssuanceRangeProofsHash, - IssuanceToken, - IssuanceTokenAmount, - IssuanceTokenAmountsHash, - IssuanceTokenProof, - IssuancesHash, - LbtcAsset, - Le16, - Le32, - Le64, - Le8, - LeftExtend16_32, - LeftExtend16_64, - LeftExtend1_16, - LeftExtend1_32, - LeftExtend1_64, - LeftExtend1_8, - LeftExtend32_64, - LeftExtend8_16, - LeftExtend8_32, - LeftExtend8_64, - LeftPadHigh16_32, - LeftPadHigh16_64, - LeftPadHigh1_16, - LeftPadHigh1_32, - LeftPadHigh1_64, - LeftPadHigh1_8, - LeftPadHigh32_64, - LeftPadHigh8_16, - LeftPadHigh8_32, - LeftPadHigh8_64, - LeftPadLow16_32, - LeftPadLow16_64, - LeftPadLow1_16, - LeftPadLow1_32, - LeftPadLow1_64, - LeftPadLow1_8, - LeftPadLow32_64, - LeftPadLow8_16, - LeftPadLow8_32, - LeftPadLow8_64, - LeftRotate16, - LeftRotate32, - LeftRotate64, - LeftRotate8, - LeftShift16, - LeftShift32, - LeftShift64, - LeftShift8, - LeftShiftWith16, - LeftShiftWith32, - LeftShiftWith64, - LeftShiftWith8, - Leftmost16_1, - Leftmost16_2, - Leftmost16_4, - Leftmost16_8, - Leftmost32_1, - Leftmost32_16, - Leftmost32_2, - Leftmost32_4, - Leftmost32_8, - Leftmost64_1, - Leftmost64_16, - Leftmost64_2, - Leftmost64_32, - Leftmost64_4, - Leftmost64_8, - Leftmost8_1, - Leftmost8_2, - Leftmost8_4, - LinearCombination1, - LinearVerify1, - LockTime, - Low1, - Low16, - Low32, - Low64, - Low8, - Lt16, - Lt32, - Lt64, - Lt8, - Maj1, - Maj16, - Maj32, - Maj64, - Maj8, - Max16, - Max32, - Max64, - Max8, - Median16, - Median32, - Median64, - Median8, - Min16, - Min32, - Min64, - Min8, - Modulo16, - Modulo32, - Modulo64, - Modulo8, - Multiply16, - Multiply32, - Multiply64, - Multiply8, - Negate16, - Negate32, - Negate64, - Negate8, - NewIssuanceContract, - NonceHash, - NumInputs, - NumOutputs, - One16, - One32, - One64, - One8, - Or1, - Or16, - Or32, - Or64, - Or8, - OutpointHash, - OutputAmount, - OutputAmountsHash, - OutputAsset, - OutputHash, - OutputIsFee, - OutputNonce, - OutputNoncesHash, - OutputNullDatum, - OutputRangeProof, - OutputRangeProofsHash, - OutputScriptHash, - OutputScriptsHash, - OutputSurjectionProof, - OutputSurjectionProofsHash, - OutputsHash, - ParseLock, - ParseSequence, - PointVerify1, - ReissuanceBlinding, - ReissuanceEntropy, - RightExtend16_32, - RightExtend16_64, - RightExtend32_64, - RightExtend8_16, - RightExtend8_32, - RightExtend8_64, - RightPadHigh16_32, - RightPadHigh16_64, - RightPadHigh1_16, - RightPadHigh1_32, - RightPadHigh1_64, - RightPadHigh1_8, - RightPadHigh32_64, - RightPadHigh8_16, - RightPadHigh8_32, - RightPadHigh8_64, - RightPadLow16_32, - RightPadLow16_64, - RightPadLow1_16, - RightPadLow1_32, - RightPadLow1_64, - RightPadLow1_8, - RightPadLow32_64, - RightPadLow8_16, - RightPadLow8_32, - RightPadLow8_64, - RightRotate16, - RightRotate32, - RightRotate64, - RightRotate8, - RightShift16, - RightShift32, - RightShift64, - RightShift8, - RightShiftWith16, - RightShiftWith32, - RightShiftWith64, - RightShiftWith8, - Rightmost16_1, - Rightmost16_2, - Rightmost16_4, - Rightmost16_8, - Rightmost32_1, - Rightmost32_16, - Rightmost32_2, - Rightmost32_4, - Rightmost32_8, - Rightmost64_1, - Rightmost64_16, - Rightmost64_2, - Rightmost64_32, - Rightmost64_4, - Rightmost64_8, - Rightmost8_1, - Rightmost8_2, - Rightmost8_4, - ScalarAdd, - ScalarInvert, - ScalarIsZero, - ScalarMultiply, - ScalarMultiplyLambda, - ScalarNegate, - ScalarNormalize, - ScalarSquare, - Scale, - ScriptCMR, - Sha256Block, - Sha256Ctx8Add1, - Sha256Ctx8Add128, - Sha256Ctx8Add16, - Sha256Ctx8Add2, - Sha256Ctx8Add256, - Sha256Ctx8Add32, - Sha256Ctx8Add4, - Sha256Ctx8Add512, - Sha256Ctx8Add64, - Sha256Ctx8Add8, - Sha256Ctx8AddBuffer511, - Sha256Ctx8Finalize, - Sha256Ctx8Init, - Sha256Iv, - SigAllHash, - Some1, - Some16, - Some32, - Some64, - Some8, - Subtract16, - Subtract32, - Subtract64, - Subtract8, - Swu, - TapEnvHash, - TapdataInit, - TapleafHash, - TapleafVersion, - Tappath, - TappathHash, - TotalFee, - TransactionId, - TxHash, - TxIsFinal, - TxLockDistance, - TxLockDuration, - TxLockHeight, - TxLockTime, - Verify, - Version, - Xor1, - Xor16, - Xor32, - Xor64, - Xor8, - XorXor1, - XorXor16, - XorXor32, - XorXor64, - XorXor8, -} diff --git a/core/src/jets/environments.rs b/core/src/jets/environments.rs index b34dcca..d288b87 100644 --- a/core/src/jets/environments.rs +++ b/core/src/jets/environments.rs @@ -1,11 +1,19 @@ -use std::sync::Arc; +use simplicity::elements::script::Script; +use simplicity::jet::{CoreEnv, ElementsTxEnv}; -use hal_simplicity::simplicity::jet::elements::ElementsEnv; +pub type BitcoinUnchainedEnv = UnchainedEnv; +pub type ElementsUnchainedEnv = UnchainedEnv; -use hal_simplicity::simplicity::elements::{Transaction, script::Script}; - -pub type BitcoinUnchainedEnv = UnchainedEnv<()>; -pub type ElementsUnchainedEnv = UnchainedEnv>>; +/// Transaction context exposed to Bitcoin custom jets and plugins. +pub struct BitcoinTxEnv { + /// Unsigned spending transaction, without PSBT signature metadata. + pub tx: simplicity::bitcoin::Transaction, + /// Input being authorized. + pub input_index: u32, + /// Versioned per-spend approval digest, distinct from the Taproot signing sighash. + pub approval_hash: [u8; 32], + pub(crate) core_env: CoreEnv, +} pub struct UnchainedEnv { pub redeem_script: Script, diff --git a/core/src/jets/exec.rs b/core/src/jets/exec.rs index a1cbca4..e76bdbb 100644 --- a/core/src/jets/exec.rs +++ b/core/src/jets/exec.rs @@ -1,23 +1,41 @@ -use hal_simplicity::simplicity::{ +use simplicity::{ elements::{Transaction, opcodes::all::OP_PUSHBYTES_33}, ffi::CFrameItem, + ffi::c_jets::frame_ffi::{c_readBit, c_writeBit}, }; use crate::jets::environments::ElementsUnchainedEnv; -use super::environments::UnchainedEnv; +use super::environments::{BitcoinUnchainedEnv, UnchainedEnv}; -#[allow(unused)] -unsafe extern "C" { - fn rustsimplicity_0_6_write8(dst: *mut CFrameItem, n: u64); - fn rustsimplicity_0_6_write16(dst: *mut CFrameItem, n: u16); - fn rustsimplicity_0_6_write32(dst: *mut CFrameItem, n: u32); - fn rustsimplicity_0_6_write64(dst: *mut CFrameItem, n: u64); +/// ONE |- 2^256 +/// +/// Returns the versioned Bitcoin transaction approval digest computed by the runner. +pub fn bitcoin_tx_approval_hash( + dst: &mut CFrameItem, + _src: CFrameItem, + env: &BitcoinUnchainedEnv, +) -> bool { + for byte in env.env.approval_hash { + write_bits(dst, byte.into(), 8); + } + true +} + +/// Reads a big-endian `n`-bit value from a C read frame. +fn read_bits(src: &mut CFrameItem, n: u32) -> u64 { + let mut value = 0; + for _ in 0..n { + value = (value << 1) | u64::from(unsafe { c_readBit(src) }); + } + value +} - fn rustsimplicity_0_6_read8(src: *const CFrameItem) -> u64; - fn rustsimplicity_0_6_read16(src: *const CFrameItem) -> u16; - fn rustsimplicity_0_6_read32(src: *const CFrameItem) -> u32; - fn rustsimplicity_0_6_read64(src: *const CFrameItem) -> u64; +/// Writes the low `n` bits of `value`, big-endian, to a C write frame. +fn write_bits(dst: &mut CFrameItem, value: u64, n: u32) { + for i in (0..n).rev() { + unsafe { c_writeBit(dst, (value >> i) & 1 == 1) }; + } } /// 2^8 |- 2^8 @@ -25,19 +43,17 @@ unsafe extern "C" { /// Returns the opcode at the given index in the redeem script, each opcode is one byte. pub fn get_opcode_from_script( dst: &mut CFrameItem, - src: CFrameItem, + mut src: CFrameItem, env: &UnchainedEnv, ) -> bool { - let index = unsafe { rustsimplicity_0_6_read8(&src as *const CFrameItem) } as usize; + let index = read_bits(&mut src, 8) as usize; if index >= env.redeem_script.len() { return false; } let opcode = env.redeem_script.as_bytes()[index]; - unsafe { - rustsimplicity_0_6_write8(dst as *mut CFrameItem, opcode as u64); - } + write_bits(dst, opcode.into(), 8); true } @@ -46,10 +62,10 @@ pub fn get_opcode_from_script( /// Each pubkey is encoded as: [OP_PUSHBYTES_33][0x02 or 0x03][32 bytes X coordinate] pub fn get_pubkey_from_script( dst: &mut CFrameItem, - src: CFrameItem, + mut src: CFrameItem, env: &UnchainedEnv, ) -> bool { - let start_index = unsafe { rustsimplicity_0_6_read8(&src as *const CFrameItem) } as usize; + let start_index = read_bits(&mut src, 8) as usize; if start_index + 34 > env.redeem_script.len() { return false; } @@ -74,9 +90,7 @@ pub fn get_pubkey_from_script( .collect::>(); for word in words.iter() { - unsafe { - rustsimplicity_0_6_write32(dst as *mut CFrameItem, *word); - } + write_bits(dst, (*word).into(), 32); } true @@ -87,7 +101,7 @@ pub fn get_pubkey_from_script( /// 2^16 |- ONE pub fn check_lock_duration( _dst: &mut CFrameItem, - src: CFrameItem, + mut src: CFrameItem, env: &ElementsUnchainedEnv, ) -> bool { let (tx, ix) = (env.env.tx(), env.env.ix()); @@ -96,14 +110,14 @@ pub fn check_lock_duration( return false; } - let req_duration = unsafe { rustsimplicity_0_6_read16(&src as *const CFrameItem) }; + let req_duration = read_bits(&mut src, 16) as u16; req_duration <= lock_duration(tx, ix) } /// 2^16 |- ONE pub fn check_lock_distance( _dst: &mut CFrameItem, - src: CFrameItem, + mut src: CFrameItem, env: &ElementsUnchainedEnv, ) -> bool { let (tx, ix) = (env.env.tx(), env.env.ix()); @@ -112,7 +126,7 @@ pub fn check_lock_distance( return false; } - let req_distance = unsafe { rustsimplicity_0_6_read16(&src as *const CFrameItem) }; + let req_distance = read_bits(&mut src, 16) as u16; req_distance <= lock_distance(tx, ix) } @@ -128,7 +142,7 @@ pub fn tx_lock_duration( return false; } - unsafe { rustsimplicity_0_6_write16(dst, lock_duration(tx, ix)) }; + write_bits(dst, lock_duration(tx, ix).into(), 16); true } @@ -144,7 +158,7 @@ pub fn tx_lock_distance( return false; } - unsafe { rustsimplicity_0_6_write16(dst, lock_distance(tx, ix)) }; + write_bits(dst, lock_distance(tx, ix).into(), 16); true } diff --git a/core/src/jets/liquid_policy.rs b/core/src/jets/liquid_policy.rs new file mode 100644 index 0000000..c0f0d2a --- /dev/null +++ b/core/src/jets/liquid_policy.rs @@ -0,0 +1 @@ +pub use simplicity_unchained_policy::liquid::PolicyJet as LiquidPolicyJet; diff --git a/core/src/jets/mod.rs b/core/src/jets/mod.rs index fa0d9a4..e00a87b 100644 --- a/core/src/jets/mod.rs +++ b/core/src/jets/mod.rs @@ -1,7 +1,211 @@ +pub mod base; pub mod bitcoin; pub mod elements; pub mod environments; pub mod exec; +pub mod liquid_policy; +pub mod policy; #[cfg(test)] -mod tests; +pub(crate) mod tests { + use std::ptr; + use std::sync::Arc; + + use hex_literal::hex; + use simplicity::Cmr; + use simplicity::elements::taproot::ControlBlock; + use simplicity::elements::{ + AssetIssuance, BlockHash, LockTime, OutPoint, Sequence, Transaction, TxIn, TxInWitness, + }; + use simplicity::jet::elements::ElementsEnv; + + use crate::jets::environments::{ElementsUnchainedEnv, UnchainedEnv}; + use crate::jets::{bitcoin::CoreExtension, elements::ElementsExtension}; + use simplicity::elements::Script; + use simplicity::elements::opcodes::all::OP_PUSHNUM_2; + use simplicity::ffi::CFrameItem; + use simplicity::ffi::c_jets::c_frame::uword_width; + use simplicity::ffi::ffi::UWORD; + use simplicity::jet::{Jet, JetEnvironment}; + use simplicity::{BitIter, BitWriter}; + + #[test] + fn test_new_jets_decode() { + for expected_jet in ElementsExtension::ALL { + let mut source = vec![]; + let mut writer = BitWriter::new(&mut source as &mut dyn std::io::Write); + + let _ = expected_jet.encode(&mut writer).unwrap(); + + writer.flush_all().unwrap(); + + let mut iter = BitIter::from(&source[..]); + + let decoded_jet = ElementsExtension::decode(&mut iter); + + assert!(decoded_jet.is_ok()); + assert_eq!(decoded_jet.unwrap(), expected_jet); + } + + for expected_jet in CoreExtension::ALL { + let mut source = vec![]; + let mut writer = BitWriter::new(&mut source as &mut dyn std::io::Write); + + let _ = expected_jet.encode(&mut writer).unwrap(); + + writer.flush_all().unwrap(); + + let mut iter = BitIter::from(&source[..]); + + let decoded_jet = CoreExtension::decode(&mut iter); + + assert!(decoded_jet.is_ok()); + assert_eq!(decoded_jet.unwrap(), expected_jet); + } + } + + #[test] + fn bitcoin_custom_jet_encodings_and_identities_are_stable() { + for (jet, expected) in [ + (CoreExtension::GetOpcodeFromScript, 0xf8u8), + (CoreExtension::GetPubkeyFromScript, 0xfc), + (CoreExtension::TxApprovalHash, 0xfe), + ] { + let mut encoded = Vec::new(); + let mut writer = BitWriter::new(&mut encoded as &mut dyn std::io::Write); + jet.encode(&mut writer).unwrap(); + writer.flush_all().unwrap(); + assert_eq!(encoded, [expected]); + } + assert_eq!( + hex::encode(CoreExtension::GetOpcodeFromScript.cmr().to_byte_array()), + "dcccd2895922e75b018b0846e5cd4963808bbfd48b47234475607f90e70ee032", + ); + assert_eq!( + hex::encode(CoreExtension::GetPubkeyFromScript.cmr().to_byte_array()), + "27eab09068b035af619713335b73d2520ecc02090067c8fccabb4d72a655cdcb", + ); + assert_eq!( + CoreExtension::parse("tx_approval_hash").unwrap(), + CoreExtension::TxApprovalHash, + ); + assert_eq!( + CoreExtension::TxApprovalHash.to_string(), + "tx_approval_hash" + ); + assert_eq!( + CoreExtension::TxApprovalHash.cmr().to_byte_array(), + simplicity::hashes::sha256::Hash::hash(b"SimplicityUnchained/Jet/tx_approval_hash/v1",) + .to_byte_array(), + ); + } + + #[test] + fn test_get_opcode() { + let script = Script::from_hex_no_prefix( + "5221033523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf21025eb4655feae15a304653e27441ca8e8ced2bef89c22ab6b20424b4c07b3d14cc52ae" + ).unwrap(); + + let env = UnchainedEnv::new(script, dummy_elements_env()); + + let output_uwords = uword_width(8); + let mut dst_data = vec![0 as UWORD; output_uwords]; + + unsafe { + let src_data: Vec = vec![0; 1]; + + let src_frame = CFrameItem::new_read(8, src_data.as_ptr()); + + let mut dst_frame = CFrameItem::new_write(8, dst_data.as_mut_ptr().add(output_uwords)); + + let jet = ElementsExtension::GetOpcodeFromScript; + let jet_fn = ElementsUnchainedEnv::c_jet_ptr(&jet); + let result = jet_fn(&mut dst_frame, src_frame, &env); + + assert!(result); + + for us in &mut dst_data { + *us = us.swap_bytes().to_be(); + } + + let opcode = dst_data[0] as u8; + + assert_eq!(opcode, OP_PUSHNUM_2.into_u8()); + } + } + + #[test] + fn test_get_pubkey_from_script() { + let script = Script::from_hex_no_prefix( + "5221033523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf21025eb4655feae15a304653e27441ca8e8ced2bef89c22ab6b20424b4c07b3d14cc52ae" + ).unwrap(); + + let env = UnchainedEnv::new(script, dummy_elements_env()); + + const OUTPUT_BITS: usize = 256; + let output_uwords = uword_width(OUTPUT_BITS); + let mut dst_data = vec![0 as UWORD; output_uwords]; + + unsafe { + let src_data: Vec = vec![1]; + + let src_frame = CFrameItem::new_read(8, src_data.as_ptr()); + + let mut dst_frame = + CFrameItem::new_write(OUTPUT_BITS, dst_data.as_mut_ptr().add(output_uwords)); + + let jet = ElementsExtension::GetPubkeyFromScript; + let jet_fn = ElementsUnchainedEnv::c_jet_ptr(&jet); + let result = jet_fn(&mut dst_frame, src_frame, &env); + + assert!(result); + + for us in &mut dst_data { + *us = us.swap_bytes().to_be(); + } + + let mut output_bytes = vec![0u8; 32]; + ptr::copy_nonoverlapping( + dst_data.as_ptr() as *const u8, + output_bytes.as_mut_ptr(), + 32, + ); + + output_bytes.reverse(); + + let expected = hex!("3523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf"); + + assert_eq!(output_bytes, expected.to_vec()); + } + } + + pub(crate) fn dummy_elements_env() -> ElementsEnv> { + let ctrl_blk: [u8; 33] = [ + 0xc0, 0xeb, 0x04, 0xb6, 0x8e, 0x9a, 0x26, 0xd1, 0x16, 0x04, 0x6c, 0x76, 0xe8, 0xff, + 0x47, 0x33, 0x2f, 0xb7, 0x1d, 0xda, 0x90, 0xff, 0x4b, 0xef, 0x53, 0x70, 0xf2, 0x52, + 0x26, 0xd3, 0xbc, 0x09, 0xfc, + ]; + + ElementsEnv::new( + Arc::new(Transaction { + version: 2, + lock_time: LockTime::ZERO, + input: vec![TxIn { + previous_output: OutPoint::default(), + is_pegin: false, + script_sig: Script::new(), + sequence: Sequence::MAX, + asset_issuance: AssetIssuance::default(), + witness: TxInWitness::default(), + }], + output: Vec::default(), + }), + vec![], + 0, + Cmr::from_byte_array([0; 32]), + ControlBlock::from_slice(&ctrl_blk).unwrap(), + None, + BlockHash::from_byte_array([0; 32]), + ) + } +} diff --git a/core/src/jets/policy.rs b/core/src/jets/policy.rs new file mode 100644 index 0000000..0a288d1 --- /dev/null +++ b/core/src/jets/policy.rs @@ -0,0 +1,230 @@ +//! Explicit, built-in-only Bitcoin policy jet namespace. + +use std::io::Write; +use std::str::FromStr; + +use simplicity::ffi::CFrameItem; +use simplicity::ffi::c_jets::frame_ffi::{c_readBit, c_writeBit}; +use simplicity::jet::type_name::TypeName; +use simplicity::jet::{CoreEnv, Jet, JetEnvironment}; +use simplicity::{BitIter, BitWriter, Cmr, Cost, decode}; +use simplicity_unchained_policy::{BitValue, Builtin, Context}; + +use super::base::{AsBaseEnv, BaseJet, JetFn, base_c_jet_ptr}; +use super::bitcoin::CoreExtension; +use super::environments::{BitcoinTxEnv, BitcoinUnchainedEnv, UnchainedEnv}; + +#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] +pub enum PolicyJet { + Legacy(CoreExtension), + Builtin(Builtin), +} + +impl PolicyJet { + pub const ALL: [Self; CoreExtension::ALL.len() + Builtin::ALL.len()] = { + let mut jets = [Self::Legacy(CoreExtension::TxApprovalHash); + CoreExtension::ALL.len() + Builtin::ALL.len()]; + let mut index = 0; + while index < CoreExtension::ALL.len() { + jets[index] = Self::Legacy(CoreExtension::ALL[index]); + index += 1; + } + let mut builtin = 0; + while builtin < Builtin::ALL.len() { + jets[index] = Self::Builtin(Builtin::ALL[builtin]); + index += 1; + builtin += 1; + } + jets + }; + + /// Enumeration position, independent of the legacy/base trampoline table. + pub fn index(self) -> usize { + match self { + Self::Legacy(jet) => jet.index(), + Self::Builtin(jet) => CoreExtension::ALL.len() + usize::from(jet.id()), + } + } +} + +/// A validated immutable policy context and a compatibility view for legacy jets. +pub struct PolicyEnv { + context: Context, + legacy: BitcoinUnchainedEnv, +} + +impl PolicyEnv { + pub fn new(context: Context) -> Self { + let legacy = UnchainedEnv::new( + simplicity::elements::Script::from(context.script_data().to_vec()), + BitcoinTxEnv { + tx: context.tx().clone(), + input_index: context.input_index() as u32, + approval_hash: context.legacy_approval_hash(), + core_env: CoreEnv::new(), + }, + ); + Self { context, legacy } + } + + pub fn context(&self) -> &Context { + &self.context + } +} + +impl AsBaseEnv for PolicyEnv { + fn base_env(&self) -> &CoreEnv { + &self.legacy.env.core_env + } +} + +impl JetEnvironment for PolicyEnv { + type Jet = PolicyJet; + type CJetEnvironment = Self; + + fn c_jet_env(&self) -> &Self { + self + } + + fn c_jet_ptr(jet: &PolicyJet) -> JetFn { + match jet { + PolicyJet::Legacy(CoreExtension::Core(core)) => base_c_jet_ptr(core), + PolicyJet::Legacy(CoreExtension::GetOpcodeFromScript) => legacy_opcode, + PolicyJet::Legacy(CoreExtension::GetPubkeyFromScript) => legacy_pubkey, + PolicyJet::Legacy(CoreExtension::TxApprovalHash) => legacy_approval, + PolicyJet::Builtin(jet) => BUILTIN_FNS[usize::from(jet.id())], + } + } +} + +fn legacy_opcode(dst: &mut CFrameItem, src: CFrameItem, env: &PolicyEnv) -> bool { + super::exec::get_opcode_from_script(dst, src, &env.legacy) +} + +fn legacy_pubkey(dst: &mut CFrameItem, src: CFrameItem, env: &PolicyEnv) -> bool { + super::exec::get_pubkey_from_script(dst, src, &env.legacy) +} + +fn legacy_approval(dst: &mut CFrameItem, src: CFrameItem, env: &PolicyEnv) -> bool { + super::exec::bitcoin_tx_approval_hash(dst, src, &env.legacy) +} + +fn builtin(dst: &mut CFrameItem, mut src: CFrameItem, env: &PolicyEnv) -> bool { + let jet = Builtin::from_id(ID).expect("fixed built-in dispatch index"); + let bit_len = jet.source_bits(); + let mut input = BitValue { + bytes: vec![0; bit_len.div_ceil(8)], + bit_len, + }; + for bit in 0..bit_len { + if unsafe { c_readBit(&mut src) } { + input.bytes[bit / 8] |= 1 << (7 - bit % 8); + } + } + let Ok(output) = env.context.evaluate(jet, &input) else { + return false; + }; + if output.validate(jet.target_bits()).is_err() { + return false; + } + // Write sum padding too: a None result must not expose stale frame contents. + for bit in 0..output.bit_len { + unsafe { c_writeBit(dst, output.bytes[bit / 8] & (1 << (7 - bit % 8)) != 0) }; + } + true +} + +macro_rules! builtin_functions { + ($($id:literal),* $(,)?) => { + [$(builtin::<$id> as JetFn),*] + }; +} + +const BUILTIN_FNS: [JetFn; 55] = builtin_functions![ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, + 50, 51, 52, 53, 54, +]; + +impl Jet for PolicyJet { + fn cmr(&self) -> Cmr { + match self { + Self::Legacy(jet) => jet.cmr(), + Self::Builtin(jet) => Cmr::from_byte_array(jet.cmr()), + } + } + + fn source_ty(&self) -> TypeName { + match self { + Self::Legacy(jet) => jet.source_ty(), + Self::Builtin(jet) => jet.source_type(), + } + } + + fn target_ty(&self) -> TypeName { + match self { + Self::Legacy(jet) => jet.target_ty(), + Self::Builtin(jet) => jet.target_type(), + } + } + + fn encode(&self, writer: &mut BitWriter<&mut dyn Write>) -> std::io::Result { + match self { + Self::Legacy(jet) => { + writer.write_bit(false)?; + jet.encode(writer).map(|bits| 1 + bits) + } + Self::Builtin(jet) => writer.write_bits_be(64 | u64::from(jet.id()), 7), + } + } + + fn decode>(bits: &mut BitIter) -> Result { + match bits.next().ok_or(decode::Error::EndOfStream)? { + false => CoreExtension::decode(bits).map(Self::Legacy), + true => { + let mut id = 0; + for _ in 0..6 { + id = (id << 1) | u8::from(bits.next().ok_or(decode::Error::EndOfStream)?); + } + Builtin::from_id(id) + .map(Self::Builtin) + .ok_or(decode::Error::InvalidJet) + } + } + } + + fn cost(&self) -> Cost { + match self { + Self::Legacy(jet) => jet.cost(), + Self::Builtin(jet) => Cost::from_milliweight(jet.cost_milliweight()), + } + } + + fn parse(name: &str) -> Result { + name.parse() + } +} + +impl std::fmt::Display for PolicyJet { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Legacy(jet) => jet.fmt(formatter), + Self::Builtin(jet) => jet.fmt(formatter), + } + } +} + +impl FromStr for PolicyJet { + type Err = simplicity::Error; + + fn from_str(name: &str) -> Result { + if let Ok(jet) = name.parse::() { + Ok(Self::Builtin(jet)) + } else { + name.parse().map(Self::Legacy) + } + } +} + +#[cfg(test)] +mod tests; diff --git a/core/src/jets/policy/tests.rs b/core/src/jets/policy/tests.rs new file mode 100644 index 0000000..0927482 --- /dev/null +++ b/core/src/jets/policy/tests.rs @@ -0,0 +1,577 @@ +use super::*; +use std::collections::HashSet; +use std::sync::Arc; + +use simplicity::bit_machine::{ExecTracker, NodeOutput}; +use simplicity::bitcoin::{Sequence, absolute}; +use simplicity::ffi::c_jets::c_frame::uword_width; +use simplicity::ffi::ffi::UWORD; +use simplicity::node::{CoreConstructible, Inner, WitnessConstructible}; +use simplicity::{BitMachine, ConstructNode, RedeemNode, Value, types}; +use simplicity_unchained_policy::Error; + +use crate::policy::tests::{Fixture, unit_program}; + +fn packed(bits: impl IntoIterator) -> BitValue { + let bits = bits.into_iter().collect::>(); + let mut bytes = vec![0; bits.len().div_ceil(8)]; + for (index, bit) in bits.iter().enumerate() { + if *bit { + bytes[index / 8] |= 1 << (7 - index % 8); + } + } + BitValue { + bytes, + bit_len: bits.len(), + } +} + +fn word(value: u64, bits: usize) -> BitValue { + packed((0..bits).rev().map(|bit| value & (1 << bit) != 0)) +} + +fn arguments(jet: Builtin, env: &PolicyEnv) -> Vec { + match jet.source_bits() { + 0 => vec![BitValue::unit()], + 16 => vec![word(0, 16), word(10, 16), word(11, 16), word(65_535, 16)], + 32 => [0, 1, 7, 8, 500, 500_000_000, u32::MAX] + .map(|value| word(value.into(), 32)) + .to_vec(), + 64 => [ + (0, 0), + (0, 1), + (0, 33), + (0, 34), + (0, 9_999), + (0, 10_000), + (8, 0), + (u32::MAX, u32::MAX), + ] + .map(|(index, offset)| { + let mut bytes = index.to_be_bytes().to_vec(); + bytes.extend_from_slice(&offset.to_be_bytes()); + BitValue { bytes, bit_len: 64 } + }) + .to_vec(), + 256 => { + use simplicity::bitcoin::hashes::{Hash, sha256}; + vec![ + BitValue { + bytes: vec![0; 32], + bit_len: 256, + }, + BitValue { + bytes: sha256::Hash::hash(env.context().prevouts()[0].script_pubkey.as_bytes()) + .to_byte_array() + .to_vec(), + bit_len: 256, + }, + BitValue { + bytes: sha256::Hash::hash( + env.context().tx().output[0].script_pubkey.as_bytes(), + ) + .to_byte_array() + .to_vec(), + bit_len: 256, + }, + ] + } + bits => panic!("unhandled source width {bits} for {jet}"), + } +} + +struct Capture(Option); + +impl ExecTracker for Capture { + fn visit_node( + &mut self, + node: &RedeemNode, + _input: simplicity::bit_machine::FrameIter, + output: NodeOutput, + ) { + if matches!(node.inner(), Inner::Jet(_)) + && let NodeOutput::Success(frame) = output + { + self.0 = Some(packed(frame.take(node.arrow().target.bit_width()))); + } + } +} + +fn vm_program(jet: PolicyJet, source: &BitValue) -> Arc { + types::Context::with_context(|ctx| { + let value = Value::from_padded_bits( + &mut BitIter::from(source.bytes.as_slice()), + &jet.source_ty().to_final(), + ) + .unwrap(); + let argument = Arc::::witness(&ctx, Some(value)); + let function = Arc::::jet(&ctx, &jet); + let apply = Arc::::comp(&argument, &function).unwrap(); + let discard = Arc::::unit(&ctx); + Arc::::comp(&apply, &discard) + .unwrap() + .finalize_unpruned() + .unwrap() + }) +} + +fn ffi_call(jet: PolicyJet, source: &BitValue, env: &PolicyEnv, offset: usize) -> (bool, BitValue) { + const GUARD: UWORD = 0x5a5a; + let target_bits = jet.target_ty().to_bit_width(); + let input_bits = offset + source.bit_len + 9; + let output_bits = offset + target_bits + 9; + let input_words = uword_width(input_bits); + let output_words = uword_width(output_bits); + let mut input = vec![GUARD; input_words + 2]; + let mut output = vec![UWORD::MAX; output_words + 2]; + output[0] = GUARD; + output[output_words + 1] = GUARD; + unsafe { + let mut frame = CFrameItem::new_write(input_bits, input.as_mut_ptr().add(input_words + 1)); + for _ in 0..offset { + c_writeBit(&mut frame, true); + } + for index in 0..source.bit_len { + c_writeBit(&mut frame, source.bit(index).unwrap()); + } + for bit in 0..9 { + c_writeBit(&mut frame, bit % 2 == 0); + } + let saved_input = input.clone(); + let mut src = CFrameItem::new_read(input_bits, input.as_ptr().add(1)); + for _ in 0..offset { + assert!(c_readBit(&mut src)); + } + let mut dst = CFrameItem::new_write(output_bits, output.as_mut_ptr().add(output_words + 1)); + for _ in 0..offset { + c_writeBit(&mut dst, true); + } + let before = dst.len; + let result = PolicyEnv::c_jet_ptr(&jet)(&mut dst, src, env); + if result { + assert_eq!(before - dst.len, target_bits, "{jet}"); + } else { + assert_eq!( + before, dst.len, + "rejected jet must not partially write: {jet}" + ); + } + assert_eq!(input, saved_input, "{jet} modified its source"); + let written = before - dst.len; + for bit in written..target_bits { + c_writeBit(&mut dst, false); + assert!(bit < target_bits); + } + for bit in 0..9 { + c_writeBit(&mut dst, bit % 2 == 0); + } + assert_eq!(output[0], GUARD, "{jet} underflow"); + assert_eq!(output[output_words + 1], GUARD, "{jet} overflow"); + let mut read = CFrameItem::new_read(output_bits, output.as_ptr().add(1)); + for _ in 0..offset { + assert!(c_readBit(&mut read), "{jet} prefix canary"); + } + let actual = packed((0..target_bits).map(|_| c_readBit(&mut read))); + for bit in 0..9 { + assert_eq!(c_readBit(&mut read), bit % 2 == 0, "{jet} suffix canary"); + } + (result, actual) + } +} + +#[test] +fn every_builtin_executes_in_vm_and_matches_reference_at_frame_offsets() { + let program = unit_program(); + let mut fixture = Fixture::new(&program, 8, 8, true); + let mut observed = HashSet::new(); + for (sequence, lock_time) in [(10, 500), ((1 << 22) | 10, 500_000_010), (u32::MAX, 500)] { + for input in &mut fixture.psbt.unsigned_tx.input { + input.sequence = Sequence::from_consensus(sequence); + } + fixture.psbt.unsigned_tx.lock_time = absolute::LockTime::from_consensus(lock_time); + let prepared = fixture.prepare(&program, 0).unwrap(); + let context = Context::new( + &fixture.psbt, + 0, + prepared.cmr().to_byte_array(), + &fixture.script, + fixture.network, + fixture.base, + fixture.user, + fixture.recovery, + ) + .unwrap(); + let env = PolicyEnv::new(context); + for builtin in Builtin::ALL { + observed.insert(builtin.id()); + let jet = PolicyJet::Builtin(builtin); + for argument in arguments(builtin, &env) { + let expected = env.context().evaluate(builtin, &argument); + let redeem = vm_program(jet, &argument); + let mut capture = Capture(None); + let result = BitMachine::for_program(&redeem).unwrap().exec_with_tracker( + &redeem, + &env, + &mut capture, + ); + match &expected { + Ok(value) => { + result.unwrap(); + assert_eq!(capture.0.as_ref(), Some(value), "{builtin} {argument:?}"); + } + Err(Error::CheckFailed) => assert!(result.is_err(), "{builtin}"), + Err(error) => panic!("unexpected reference error {builtin}: {error}"), + } + for offset in [0, 1, 3, 7] { + let (ok, output) = ffi_call(jet, &argument, &env, offset); + assert_eq!(ok, expected.is_ok(), "{builtin} offset={offset}"); + if let Ok(expected) = &expected { + assert_eq!(&output, expected, "{builtin} offset={offset}"); + } + } + } + } + } + assert_eq!(observed.len(), 55); +} + +#[test] +fn shared_core_and_all_three_legacy_jets_execute_in_policy_environment() { + let program = unit_program(); + let fixture = Fixture::new(&program, 2, 2, false); + let prepared = fixture.prepare(&program, 1).unwrap(); + let context = Context::new( + &fixture.psbt, + 1, + prepared.cmr().to_byte_array(), + &fixture.script, + fixture.network, + fixture.base, + fixture.user, + fixture.recovery, + ) + .unwrap(); + let env = PolicyEnv::new(context); + for (jet, argument, expected) in [ + ( + CoreExtension::Core(simplicity::jet::Core::Eq32), + word(0, 64), + word(1, 1), + ), + ( + CoreExtension::GetOpcodeFromScript, + word(0, 8), + word(0x21, 8), + ), + ( + CoreExtension::GetPubkeyFromScript, + word(0, 8), + BitValue { + bytes: fixture.user.serialize().to_vec(), + bit_len: 256, + }, + ), + ( + CoreExtension::TxApprovalHash, + BitValue::unit(), + BitValue { + bytes: env.context().legacy_approval_hash().to_vec(), + bit_len: 256, + }, + ), + ] { + let jet = PolicyJet::Legacy(jet); + let redeem = vm_program(jet, &argument); + let mut capture = Capture(None); + BitMachine::for_program(&redeem) + .unwrap() + .exec_with_tracker(&redeem, &env, &mut capture) + .unwrap(); + assert_eq!(capture.0, Some(expected.clone()), "{jet}"); + if !matches!(jet, PolicyJet::Legacy(CoreExtension::Core(_))) { + assert_eq!(ffi_call(jet, &argument, &env, 3), (true, expected)); + } + } +} + +#[test] +fn namespace_metadata_indices_and_all_offset_roundtrips_are_complete() { + let mut cmrs = HashSet::new(); + let mut names = HashSet::new(); + for (index, jet) in PolicyJet::ALL.into_iter().enumerate() { + assert_eq!(jet.index(), index); + assert!(cmrs.insert(jet.cmr())); + assert!(names.insert(jet.to_string())); + assert_eq!(jet.to_string().parse::().unwrap(), jet); + assert_eq!(PolicyJet::parse(&jet.to_string()).unwrap(), jet); + assert_eq!( + jet.source_ty().to_bit_width(), + jet.source_ty().to_final().bit_width() + ); + assert_eq!( + jet.target_ty().to_bit_width(), + jet.target_ty().to_final().bit_width() + ); + match jet { + PolicyJet::Legacy(legacy) => { + assert_eq!(jet.cmr(), legacy.cmr()); + assert_eq!(jet.source_ty(), legacy.source_ty()); + assert_eq!(jet.target_ty(), legacy.target_ty()); + assert_eq!(jet.cost(), legacy.cost()); + } + PolicyJet::Builtin(builtin) => { + assert_eq!(jet.source_ty().to_bit_width(), builtin.source_bits()); + assert_eq!(jet.target_ty().to_bit_width(), builtin.target_bits()); + assert_eq!(jet.cmr().to_byte_array(), builtin.cmr()); + assert_eq!(jet.cost(), Cost::from_milliweight(10_000)); + } + } + for offset in 0..8 { + let mut encoded = Vec::new(); + let mut writer = BitWriter::new(&mut encoded as &mut dyn Write); + for _ in 0..offset { + writer.write_bit(true).unwrap(); + } + let length = jet.encode(&mut writer).unwrap(); + writer.write_bits_be(0x55, 7).unwrap(); + writer.flush_all().unwrap(); + let mut bits = BitIter::byte_slice_window(&encoded, offset, offset + length + 7); + assert_eq!( + PolicyJet::decode(&mut bits).unwrap(), + jet, + "offset={offset}" + ); + assert_eq!(bits.n_total_read(), length); + for bit in (0..7).rev() { + assert_eq!(bits.next(), Some((0x55 >> bit) & 1 != 0)); + } + for _ in offset + length + 7..encoded.len() * 8 { + assert_eq!(bits.next(), Some(false)); + } + assert_eq!(bits.next(), None); + } + } + assert!("definitely_not_a_policy_jet".parse::().is_err()); +} + +#[test] +fn reserved_codes_and_truncated_prefixes_fail_at_every_bit_offset() { + for offset in 0..8 { + for id in 0..64 { + let mut bytes = Vec::new(); + let mut writer = BitWriter::new(&mut bytes as &mut dyn Write); + for _ in 0..offset { + writer.write_bit(false).unwrap(); + } + writer.write_bits_be(64 | id, 7).unwrap(); + writer.flush_all().unwrap(); + let mut bits = BitIter::byte_slice_window(&bytes, offset, offset + 7); + let decoded = PolicyJet::decode(&mut bits); + if id < 55 { + assert_eq!( + decoded.unwrap(), + PolicyJet::Builtin(Builtin::from_id(id as u8).unwrap()) + ); + } else { + assert!(matches!(decoded, Err(decode::Error::InvalidJet))); + } + } + } + for jet in PolicyJet::ALL { + let mut bytes = Vec::new(); + let mut writer = BitWriter::new(&mut bytes as &mut dyn Write); + let length = jet.encode(&mut writer).unwrap(); + writer.flush_all().unwrap(); + for prefix in 0..length { + // BitIter only bounds whole bytes at its right edge. Right-align a + // truncated prefix so padding cannot accidentally finish the code. + let offset = (8 - prefix % 8) % 8; + let mut truncated = Vec::new(); + let mut writer = BitWriter::new(&mut truncated as &mut dyn Write); + for _ in 0..offset { + writer.write_bit(false).unwrap(); + } + for index in 0..prefix { + writer + .write_bit(bytes[index / 8] >> (7 - index % 8) & 1u8 != 0u8) + .unwrap(); + } + writer.flush_all().unwrap(); + let mut bits = BitIter::byte_slice_window(&truncated, offset, offset + prefix); + let result = PolicyJet::decode(&mut bits); + assert!(result.is_err(), "{jet} offset={offset} prefix={prefix}"); + if matches!(jet, PolicyJet::Builtin(_)) { + assert!(matches!(result, Err(decode::Error::EndOfStream))); + } + } + } +} + +#[test] +fn fixed_namespace_vectors_and_option_widths() { + for (jet, byte) in [ + (PolicyJet::Builtin(Builtin::PolicyProfileHash), 0x80u8), + (PolicyJet::Builtin(Builtin::TxVsizeLowerBound), 0xec), + (PolicyJet::Legacy(CoreExtension::GetOpcodeFromScript), 0x7c), + (PolicyJet::Legacy(CoreExtension::GetPubkeyFromScript), 0x7e), + (PolicyJet::Legacy(CoreExtension::TxApprovalHash), 0x7f), + ] { + let mut bytes = Vec::new(); + let mut writer = BitWriter::new(&mut bytes as &mut dyn Write); + jet.encode(&mut writer).unwrap(); + writer.flush_all().unwrap(); + assert_eq!(bytes, [byte]); + } + for (name, width) in [ + ("input_prev_outpoint", 289), + ("input_value", 65), + ("input_script_hash", 257), + ("input_script_size", 33), + ("input_script_byte", 9), + ("input_sequence", 33), + ("output_script_class", 9), + ("input_lock_blocks", 17), + ("current_lock_intervals", 17), + ] { + let jet: PolicyJet = name.parse().unwrap(); + assert_eq!(jet.target_ty().to_bit_width(), width, "{name}"); + } +} + +#[test] +fn all_policy_type_names_match_the_frozen_contract() { + let types: [(&[u8], &[u8]); 55] = [ + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"i"), + (b"1", b"i"), + (b"1", b"i"), + (b"1", b"i"), + (b"1", b"i"), + (b"i", b"+1*hi"), + (b"i", b"+1l"), + (b"i", b"+1h"), + (b"i", b"+1i"), + (b"*ii", b"+1c"), + (b"i", b"+1i"), + (b"1", b"*hi"), + (b"1", b"l"), + (b"1", b"h"), + (b"1", b"i"), + (b"i", b"+1l"), + (b"i", b"+1h"), + (b"i", b"+1i"), + (b"*ii", b"+1c"), + (b"i", b"+1c"), + (b"1", b"l"), + (b"1", b"l"), + (b"1", b"l"), + (b"h", b"l"), + (b"h", b"l"), + (b"h", b"i"), + (b"1", b"l"), + (b"1", b"l"), + (b"1", b"2"), + (b"1", b"2"), + (b"i", b"+1s"), + (b"i", b"+1s"), + (b"1", b"+1s"), + (b"1", b"+1s"), + (b"s", b"1"), + (b"s", b"1"), + (b"i", b"1"), + (b"i", b"1"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"h"), + (b"1", b"i"), + ]; + for (jet, (source, target)) in Builtin::ALL.into_iter().zip(types) { + let wrapper = PolicyJet::Builtin(jet); + assert_eq!(wrapper.source_ty().0, source, "{jet}"); + assert_eq!(wrapper.target_ty().0, target, "{jet}"); + } +} + +#[test] +#[ignore = "bounded local timing report; not a consensus-cost calibration"] +fn timing_at_profile_maxshape_and_execution_cap() { + use std::hint::black_box; + use std::time::Instant; + let program = unit_program(); + let fixture = Fixture::new(&program, 8, 8, true); + let started = Instant::now(); + let prepared = fixture.prepare(&program, 0).unwrap(); + let preparation = started.elapsed(); + let context = Context::new( + &fixture.psbt, + 0, + prepared.cmr().to_byte_array(), + &fixture.script, + fixture.network, + fixture.base, + fixture.user, + fixture.recovery, + ) + .unwrap(); + let env = PolicyEnv::new(context); + let mut calls = 0; + let started = Instant::now(); + for _ in 0..10 { + for builtin in Builtin::ALL { + let argument = arguments(builtin, &env).remove(0); + let jet = PolicyJet::Builtin(builtin); + let redeem = vm_program(jet, &argument); + black_box( + BitMachine::for_program(&redeem) + .unwrap() + .exec(&redeem, &env), + ) + .ok(); + calls += 1; + } + } + eprintln!( + "experimental policy timing: 8 inputs, 8 x 10000-byte scripts; prepare={preparation:?}, {calls} VM calls={:?}; 10000mw/jet is a conservative application budget, not consensus calibration", + started.elapsed() + ); + assert!(calls * 10_000 <= 10_000_000); + let program = types::Context::with_context(|ctx| { + let current = + Arc::::jet(&ctx, &PolicyJet::Builtin(Builtin::CurrentScriptHash)); + let aggregate = + Arc::::jet(&ctx, &PolicyJet::Builtin(Builtin::InputValueForScript)); + let call = Arc::::comp(¤t, &aggregate).unwrap(); + let unit = Arc::::unit(&ctx); + let call = Arc::::comp(&call, &unit).unwrap(); + let mut program = Arc::clone(&call); + for _ in 1..450 { + program = Arc::::comp(&program, &call).unwrap(); + } + program.finalize_types().unwrap().to_vec_without_witness() + }); + let fixture = Fixture::new(&program, 8, 8, true); + let started = Instant::now(); + let prepared = fixture.prepare(&program, 0).unwrap(); + let construction = started.elapsed(); + let started = Instant::now(); + let evaluation = prepared.evaluate_default(&[]).unwrap(); + eprintln!( + "near-cap actual policy: cost={}mw, prepare={construction:?}, evaluate={:?} (includes decode, cost check, allocation and 900 jet calls)", + evaluation.cost_milliweight, + started.elapsed(), + ); + assert!(evaluation.cost_milliweight >= 9_000_000); + assert!(evaluation.cost_milliweight <= 10_000_000); +} diff --git a/core/src/jets/tests.rs b/core/src/jets/tests.rs deleted file mode 100644 index 0f80ad1..0000000 --- a/core/src/jets/tests.rs +++ /dev/null @@ -1,167 +0,0 @@ -#[cfg(test)] -mod tests { - use std::ptr; - use std::sync::Arc; - - use hal_simplicity::simplicity::Cmr; - use hal_simplicity::simplicity::elements::taproot::ControlBlock; - use hal_simplicity::simplicity::elements::{ - AssetIssuance, BlockHash, LockTime, OutPoint, Sequence, Transaction, TxIn, TxInWitness, - hashes::Hash, - }; - use hal_simplicity::simplicity::jet::elements::ElementsEnv; - use hex_literal::hex; - - use crate::jets::environments::UnchainedEnv; - use crate::jets::{bitcoin::CoreExtension, elements::ElementsExtension}; - use hal_simplicity::simplicity::elements::Script; - use hal_simplicity::simplicity::elements::hex::FromHex; - use hal_simplicity::simplicity::elements::opcodes::all::OP_PUSHNUM_2; - use hal_simplicity::simplicity::ffi::CFrameItem; - use hal_simplicity::simplicity::ffi::c_jets::c_frame::uword_width; - use hal_simplicity::simplicity::ffi::ffi::UWORD; - use hal_simplicity::simplicity::jet::Jet; - use hal_simplicity::simplicity::{BitIter, BitWriter}; - - #[test] - fn test_new_jets_decode() { - for expected_jet in ElementsExtension::ALL { - let mut source = vec![]; - let mut writer = BitWriter::new(&mut source); - - let _ = expected_jet.encode(&mut writer).unwrap(); - - writer.flush_all().unwrap(); - - let mut iter = BitIter::from(&source[..]); - - let decoded_jet = ElementsExtension::decode(&mut iter).unwrap(); - - assert_eq!(decoded_jet, expected_jet); - } - - for expected_jet in CoreExtension::ALL { - let mut source = vec![]; - let mut writer = BitWriter::new(&mut source); - - let _ = expected_jet.encode(&mut writer).unwrap(); - - writer.flush_all().unwrap(); - - let mut iter = BitIter::from(&source[..]); - - let decoded_jet = CoreExtension::decode(&mut iter).unwrap(); - - assert_eq!(decoded_jet, expected_jet); - } - } - - #[test] - fn test_get_opcode() { - let script = Script::from_hex( - "5221033523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf21025eb4655feae15a304653e27441ca8e8ced2bef89c22ab6b20424b4c07b3d14cc52ae" - ).unwrap(); - - let env = UnchainedEnv::new(script, dummy_elements_env()); - - let output_uwords = uword_width(8); - let mut dst_data = vec![0 as UWORD; output_uwords]; - - unsafe { - let src_data: Vec = vec![0; 1]; - - let src_frame = CFrameItem::new_read(8, src_data.as_ptr()); - - let mut dst_frame = CFrameItem::new_write(8, dst_data.as_mut_ptr().add(output_uwords)); - - let jet = ElementsExtension::GetOpcodeFromScript; - let jet_fn = jet.c_jet_ptr(); - let result = jet_fn(&mut dst_frame, src_frame, &env); - - assert!(result); - - for us in &mut dst_data { - *us = us.swap_bytes().to_be(); - } - - let opcode = dst_data[0] as u8; - - assert_eq!(opcode, OP_PUSHNUM_2.into_u8()); - } - } - - #[test] - fn test_get_pubkey_from_script() { - let script = Script::from_hex( - "5221033523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf21025eb4655feae15a304653e27441ca8e8ced2bef89c22ab6b20424b4c07b3d14cc52ae" - ).unwrap(); - - let env = UnchainedEnv::new(script, dummy_elements_env()); - - const OUTPUT_BITS: usize = 256; - let output_uwords = uword_width(OUTPUT_BITS); - let mut dst_data = vec![0 as UWORD; output_uwords]; - - unsafe { - let src_data: Vec = vec![1]; - - let src_frame = CFrameItem::new_read(8, src_data.as_ptr()); - - let mut dst_frame = - CFrameItem::new_write(OUTPUT_BITS, dst_data.as_mut_ptr().add(output_uwords)); - - let jet = ElementsExtension::GetPubkeyFromScript; - let jet_fn = jet.c_jet_ptr(); - let result = jet_fn(&mut dst_frame, src_frame, &env); - - assert!(result); - - for us in &mut dst_data { - *us = us.swap_bytes().to_be(); - } - - let mut output_bytes = vec![0u8; 32]; - ptr::copy_nonoverlapping( - dst_data.as_ptr() as *const u8, - output_bytes.as_mut_ptr(), - 32, - ); - - output_bytes.reverse(); - - let expected = hex!("3523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf"); - - assert_eq!(output_bytes, expected.to_vec()); - } - } - - fn dummy_elements_env() -> ElementsEnv> { - let ctrl_blk: [u8; 33] = [ - 0xc0, 0xeb, 0x04, 0xb6, 0x8e, 0x9a, 0x26, 0xd1, 0x16, 0x04, 0x6c, 0x76, 0xe8, 0xff, - 0x47, 0x33, 0x2f, 0xb7, 0x1d, 0xda, 0x90, 0xff, 0x4b, 0xef, 0x53, 0x70, 0xf2, 0x52, - 0x26, 0xd3, 0xbc, 0x09, 0xfc, - ]; - - ElementsEnv::new( - Arc::new(Transaction { - version: 2, - lock_time: LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint::default(), - is_pegin: false, - script_sig: Script::new(), - sequence: Sequence::MAX, - asset_issuance: AssetIssuance::default(), - witness: TxInWitness::default(), - }], - output: Vec::default(), - }), - vec![], - 0, - Cmr::from_byte_array([0; 32]), - ControlBlock::from_slice(&ctrl_blk).unwrap(), - None, - BlockHash::all_zeros(), - ) - } -} diff --git a/core/src/lib.rs b/core/src/lib.rs index c470803..f5f1e0f 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -1,9 +1,13 @@ use std::str::FromStr; -use hal_simplicity::simplicity::elements::BlockHash; -use hal_simplicity::simplicity::elements::hashes::Hash; +use simplicity::bitcoin::NetworkKind; +use simplicity::elements::BlockHash; pub mod jets; +pub mod liquid_policy; +pub mod plugin; +pub mod policy; +pub mod program; pub mod runner; pub mod utils; @@ -61,3 +65,12 @@ impl FromStr for BitcoinNetwork { } } } + +impl From for NetworkKind { + fn from(value: BitcoinNetwork) -> Self { + match value { + BitcoinNetwork::Mainnet => Self::Main, + BitcoinNetwork::Testnet | BitcoinNetwork::Testnet4 => Self::Test, + } + } +} diff --git a/core/src/liquid_policy.rs b/core/src/liquid_policy.rs new file mode 100644 index 0000000..8b1b247 --- /dev/null +++ b/core/src/liquid_policy.rs @@ -0,0 +1,2 @@ +//! Native entry point reuses the portable, WASM-compatible Liquid policy VM. +pub use simplicity_unchained_policy::liquid::{Error, PreparedPolicy, policy_cmr}; diff --git a/core/src/plugin/README.md b/core/src/plugin/README.md new file mode 100644 index 0000000..bf4b9dd --- /dev/null +++ b/core/src/plugin/README.md @@ -0,0 +1,25 @@ +# Custom jet plugins + +## General plugin loading flow + +1. The plugin provider writes its jets as `unsafe extern "C"` functions and lists them in + `jet_plugin_abi::register_jets!`. The macro creates a static, plain-data descriptor + (ABI version, `simplicity-sys` version, base jet set) with an array of jets, each holding + its name, types, cost, encoding, CMR and function pointer, and exports a function + returning it from the dynamic library. +2. The host uses `declare_plugin!` to declare a handle type and generate its `PluginTrait` + impl. `Handle::load(path, sha256)` then loads the library, reads its descriptor, validates + it and stores the result in the handle for the rest of the process. +3. Programs are parsed and run with `PluginJet` and `PluginEnv`. Base jets run + through the host's own implementation; custom jets are called through the function + pointers from the descriptor. + +## Policy integration + +Native plugins are loaded by a Rust host, not by the policy service endpoints. +`bitcoin-policy-v1` and `liquid-policy-v1` use fixed portable registries. Adding a +jet to a policy profile requires a versioned registry and matching compiler, VM, +service and WASM implementations. + +See the [script_jets example](../../../plugin_tests/script_jets/src/lib.rs) for +a plugin implementation and the [loader test](tests.rs) for host setup. diff --git a/core/src/plugin/decode.rs b/core/src/plugin/decode.rs new file mode 100644 index 0000000..cd0ad86 --- /dev/null +++ b/core/src/plugin/decode.rs @@ -0,0 +1,145 @@ +//! Prefix tree over jet bit encodings. +//! +//! Built once at load from the base jets' encodings and the plugin's codes, so decoding never +//! needs to rewind the bit iterator and encoding conflicts are found while inserting. +use simplicity::{BitIter, decode}; + +/// Position of a node in `DecodeTrie::nodes`. +#[derive(Clone, Copy)] +struct NodeIdx(usize); + +const ROOT: NodeIdx = NodeIdx(0); + +#[derive(Default)] +struct Node { + /// Next node after reading bit 0. + zero: Option, + /// Next node after reading bit 1. + one: Option, + jet: Option, +} + +impl Node { + fn child(&self, bit: bool) -> Option { + if bit { self.one } else { self.zero } + } + + fn child_mut(&mut self, bit: bool) -> &mut Option { + if bit { &mut self.one } else { &mut self.zero } + } +} + +pub(crate) struct DecodeTrie { + nodes: Vec, +} + +impl DecodeTrie { + pub(crate) fn new() -> Self { + Self { + nodes: vec![Node::default()], + } + } + + fn node(&self, idx: NodeIdx) -> &Node { + &self.nodes[idx.0] + } + + fn node_mut(&mut self, idx: NodeIdx) -> &mut Node { + &mut self.nodes[idx.0] + } + + /// Inserts `jet` under `bits`. On conflict returns a jet whose encoding is a prefix of + /// `bits` or has `bits` as a prefix. + pub(crate) fn insert( + &mut self, + bits: impl IntoIterator, + jet: usize, + ) -> Result<(), usize> { + let mut node = ROOT; + for bit in bits { + if let Some(other) = self.node(node).jet { + return Err(other); + } + node = match self.node(node).child(bit) { + Some(child) => child, + None => { + let child = NodeIdx(self.nodes.len()); + self.nodes.push(Node::default()); + *self.node_mut(node).child_mut(bit) = Some(child); + child + } + }; + } + if let Some(other) = self.node(node).jet { + return Err(other); + } + if let Some(other) = self.any_jet_below(node) { + return Err(other); + } + self.node_mut(node).jet = Some(jet); + Ok(()) + } + + fn any_jet_below(&self, node: NodeIdx) -> Option { + let mut stack = vec![node]; + while let Some(node) = stack.pop() { + if let Some(jet) = self.node(node).jet { + return Some(jet); + } + let node = self.node(node); + stack.extend(node.zero); + stack.extend(node.one); + } + None + } + + /// Reads one jet encoding from `bits`. + pub(crate) fn decode>( + &self, + bits: &mut BitIter, + ) -> Result { + let mut node = ROOT; + loop { + if let Some(jet) = self.node(node).jet { + return Ok(jet); + } + let bit = bits.next().ok_or(decode::Error::EndOfStream)?; + node = self + .node(node) + .child(bit) + .ok_or(decode::Error::InvalidJet)?; + } + } +} + +/// The low `len` bits of `code`, most significant first. +pub(crate) fn code_bits(code: u64, len: u8) -> impl Iterator { + (0..len).rev().map(move |i| (code >> i) & 1 == 1) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn rejects_prefix_conflicts() { + let mut trie = DecodeTrie::new(); + trie.insert(code_bits(0b10, 2), 0).unwrap(); + assert_eq!(trie.insert(code_bits(0b101, 3), 1), Err(0)); + assert_eq!(trie.insert(code_bits(0b1, 1), 2), Err(0)); + assert_eq!(trie.insert(code_bits(0b10, 2), 3), Err(0)); + trie.insert(code_bits(0b11, 2), 4).unwrap(); + } + + #[test] + fn decodes_inserted_codes() { + let mut trie = DecodeTrie::new(); + trie.insert(code_bits(0b0, 1), 0).unwrap(); + trie.insert(code_bits(0b10, 2), 1).unwrap(); + + let bytes = [0b1000_0000]; + let mut bits = BitIter::from(&bytes[..]); + assert_eq!(trie.decode(&mut bits).unwrap(), 1); + assert_eq!(trie.decode(&mut bits).unwrap(), 0); + } +} diff --git a/core/src/plugin/env.rs b/core/src/plugin/env.rs new file mode 100644 index 0000000..3f3a5f2 --- /dev/null +++ b/core/src/plugin/env.rs @@ -0,0 +1,119 @@ +use jet_plugin_abi::utils::Bytes; +use jet_plugin_abi::{BaseSet, JetEnv}; +use simplicity::elements::encode::serialize; +use simplicity::ffi::CFrameItem; +use simplicity::jet::JetEnvironment; + +use super::PluginTrait; +use super::jet::PluginJet; +use super::loaded::CustomJet; +use super::trampolines::CustomJetSlots; +use crate::jets::base::{AsBaseEnv, BaseJet, base_c_jet_ptr}; +use crate::jets::bitcoin::CoreExtension; +use crate::jets::elements::ElementsExtension; + +/// A base jet set plugins can extend, with the data custom jets get to see. +pub trait PluginBase: BaseJet { + /// The ABI identifier of this set. + const ABI_BASE_SET: BaseSet; + + fn redeem_script(env: &Self::Env) -> &[u8]; + + /// Consensus-serialized spending transaction, empty if the environment has none. + fn tx(env: &Self::Env) -> Vec; + + fn input_index(env: &Self::Env) -> u32; +} + +impl PluginBase for CoreExtension { + const ABI_BASE_SET: BaseSet = BaseSet::Bitcoin; + + fn redeem_script(env: &Self::Env) -> &[u8] { + env.redeem_script.as_bytes() + } + + fn tx(env: &Self::Env) -> Vec { + simplicity::bitcoin::consensus::serialize(&env.env.tx) + } + + fn input_index(env: &Self::Env) -> u32 { + env.env.input_index + } +} + +impl PluginBase for ElementsExtension { + const ABI_BASE_SET: BaseSet = BaseSet::Elements; + + fn redeem_script(env: &Self::Env) -> &[u8] { + env.redeem_script.as_bytes() + } + + fn tx(env: &Self::Env) -> Vec { + serialize(env.env.tx()) + } + + fn input_index(env: &Self::Env) -> u32 { + env.env.ix() + } +} + +/// Environment for programs using the jets of plugin `S`. +pub struct PluginEnv { + inner: ::Env, + tx: Vec, +} + +impl PluginEnv { + pub fn new(inner: ::Env) -> Self { + let tx = S::Base::tx(&inner); + Self { inner, tx } + } + + pub fn inner(&self) -> &::Env { + &self.inner + } + + pub(super) fn call(&self, jet: &CustomJet, dst: &mut CFrameItem, src: CFrameItem) -> bool { + let env = JetEnv { + redeem_script: bytes(S::Base::redeem_script(&self.inner)), + tx: bytes(&self.tx), + input_index: S::Base::input_index(&self.inner), + }; + + // SAFETY: `env` outlives the call; plugins are operator-trusted to follow the ABI, + // including staying within their frames. + unsafe { (jet.func)(dst, src, &env) } + } +} + +impl AsBaseEnv<::Env> for PluginEnv { + fn base_env(&self) -> &::Env { + &self.inner + } +} + +impl JetEnvironment for PluginEnv { + type Jet = PluginJet; + type CJetEnvironment = Self; + + fn c_jet_env(&self) -> &Self::CJetEnvironment { + self + } + + fn c_jet_ptr( + jet: &Self::Jet, + ) -> fn(&mut CFrameItem, CFrameItem, &Self::CJetEnvironment) -> bool { + let base = ::ALL; + match jet.index().checked_sub(base.len()) { + None => base_c_jet_ptr::(&base[jet.index()]), + Some(k) => CustomJetSlots::::TABLE[k], + } + } +} + +fn bytes(slice: &[u8]) -> Bytes { + Bytes { + ptr: slice.as_ptr(), + len: slice.len(), + } +} diff --git a/core/src/plugin/jet.rs b/core/src/plugin/jet.rs new file mode 100644 index 0000000..51ec89a --- /dev/null +++ b/core/src/plugin/jet.rs @@ -0,0 +1,162 @@ +use std::cmp::Ordering; +use std::fmt; +use std::hash::{Hash, Hasher}; +use std::io::Write; +use std::marker::PhantomData; + +use simplicity::jet::Jet; +use simplicity::jet::type_name::TypeName; +use simplicity::{BitIter, BitWriter, Cmr, Cost, decode}; + +use super::PluginTrait; +use super::loaded::{CustomJet, LoadedPlugin}; +use crate::jets::base::BaseJet; + +/// A jet of plugin `S`: a base jet of `S::Base` or one of the plugin's custom jets. +/// +/// Values only exist once `S` is loaded: they are created by decoding, parsing or [`all`]. +/// +/// [`all`]: PluginJet::all +pub struct PluginJet { + /// Position in `S::Base::ALL`, followed by the custom jets in descriptor order. + index: usize, + _plugin: PhantomData S>, +} + +enum Kind { + Base(&'static B), + Custom(&'static CustomJet), +} + +impl PluginJet { + fn new(index: usize) -> Self { + Self { + index, + _plugin: PhantomData, + } + } + + fn loaded() -> &'static LoadedPlugin { + S::get().expect("a PluginJet only exists once its plugin is loaded") + } + + fn kind(&self) -> Kind { + let base = ::ALL; + match self.index.checked_sub(base.len()) { + None => Kind::Base(&base[self.index]), + Some(k) => Kind::Custom(&Self::loaded().custom[k]), + } + } + + pub(crate) fn index(&self) -> usize { + self.index + } + + /// All base and custom jets of `S`, or nothing if `S` is not loaded. + pub fn all() -> Vec { + let Some(plugin) = S::get() else { + return vec![]; + }; + (0..plugin.base_len() + plugin.custom.len()) + .map(Self::new) + .collect() + } +} + +impl Jet for PluginJet { + fn cmr(&self) -> Cmr { + match self.kind() { + Kind::Base(jet) => jet.cmr(), + Kind::Custom(jet) => jet.cmr, + } + } + + fn source_ty(&self) -> TypeName { + match self.kind() { + Kind::Base(jet) => jet.source_ty(), + Kind::Custom(jet) => jet.source_ty.clone(), + } + } + + fn target_ty(&self) -> TypeName { + match self.kind() { + Kind::Base(jet) => jet.target_ty(), + Kind::Custom(jet) => jet.target_ty.clone(), + } + } + + fn encode(&self, w: &mut BitWriter<&mut dyn Write>) -> std::io::Result { + match self.kind() { + Kind::Base(jet) => jet.encode(w), + Kind::Custom(jet) => w.write_bits_be(jet.code, usize::from(jet.code_len)), + } + } + + fn decode>(bits: &mut BitIter) -> Result { + let plugin = S::get().ok_or(decode::Error::InvalidJet)?; + plugin.trie.decode(bits).map(Self::new) + } + + fn cost(&self) -> Cost { + match self.kind() { + Kind::Base(jet) => jet.cost(), + Kind::Custom(jet) => jet.cost, + } + } + + fn parse(s: &str) -> Result { + S::get() + .and_then(|plugin| plugin.names.get(s)) + .map(|&index| Self::new(index)) + .ok_or_else(|| simplicity::Error::InvalidJetName(s.into())) + } +} + +impl fmt::Display for PluginJet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.kind() { + Kind::Base(jet) => fmt::Display::fmt(jet, f), + Kind::Custom(jet) => f.write_str(jet.name), + } + } +} + +impl fmt::Debug for PluginJet { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "PluginJet({self})") + } +} + +impl Clone for PluginJet { + fn clone(&self) -> Self { + *self + } +} + +impl Copy for PluginJet {} + +impl PartialEq for PluginJet { + fn eq(&self, other: &Self) -> bool { + self.index == other.index + } +} + +impl Eq for PluginJet {} + +impl PartialOrd for PluginJet { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for PluginJet { + fn cmp(&self, other: &Self) -> Ordering { + self.index.cmp(&other.index) + } +} + +impl Hash for PluginJet { + fn hash(&self, state: &mut H) { + self.index.hash(state) + } +} diff --git a/core/src/plugin/loaded.rs b/core/src/plugin/loaded.rs new file mode 100644 index 0000000..d671e14 --- /dev/null +++ b/core/src/plugin/loaded.rs @@ -0,0 +1,295 @@ +//! Validation of plugin descriptors into owned, host-side jet tables. +use std::collections::{HashMap, HashSet}; + +use jet_plugin_abi::{ + ABI_VERSION, BaseSet, CustomJetDesc, JetFn, PluginDescriptor, SIMPLICITY_SYS_VERSION, +}; +use simplicity::jet::Jet; +use simplicity::jet::type_name::TypeName; +use simplicity::{BitWriter, Cmr, Cost}; + +use super::decode::{DecodeTrie, code_bits}; +use super::env::PluginBase; + +/// Maximum number of custom jets in one plugin, see `trampolines::CustomTrampolines`. +pub const MAX_CUSTOM_JETS: usize = 32; + +const MAX_NAME_LEN: usize = 64; +const MAX_TYPE_NAME_LEN: usize = 256; +const MAX_CODE_LEN: u8 = 64; + +#[derive(Debug, thiserror::Error)] +pub enum LoadError { + #[error("failed to read plugin file: {0}")] + Io(#[from] std::io::Error), + + #[error("plugin file hash {found} does not match pinned hash {expected}")] + HashMismatch { expected: String, found: String }, + + #[error("failed to open plugin: {0}")] + Open(#[from] dlopen2::Error), + + #[error("plugin returned a null descriptor")] + NullDescriptor, + + #[error("plugin ABI version {found}, host expects {expected}")] + AbiVersion { expected: u32, found: u32 }, + + #[error("plugin links simplicity-sys {found}, host links {expected}")] + SimplicitySysVersion { + expected: &'static str, + found: String, + }, + + #[error("plugin extends unknown base set {0}")] + UnknownBaseSet(u32), + + #[error("plugin extends base set {found:?}, host expects {expected:?}")] + BaseSet { expected: BaseSet, found: BaseSet }, + + #[error("plugin declares {found} jets, at most {max} are supported")] + TooManyJets { max: usize, found: usize }, + + #[error("plugin declares jets through a null pointer")] + NullJets, + + #[error("custom jet #{index} has an invalid name")] + InvalidName { index: usize }, + + #[error("custom jet `{name}` is already defined")] + DuplicateName { name: String }, + + #[error("custom jet `{name}` has an invalid type `{ty}`")] + InvalidType { name: String, ty: String }, + + #[error("custom jet `{name}` has an invalid cost {cost}")] + InvalidCost { name: String, cost: u64 }, + + #[error("custom jet `{name}` has an invalid encoding")] + InvalidCode { name: String }, + + #[error("custom jet `{name}` encoding conflicts with `{other}`")] + EncodingConflict { name: String, other: String }, + + #[error("custom jet `{name}` CMR is already used by `{other}`")] + DuplicateCmr { name: String, other: String }, + + #[error("custom jet `{name}` has no implementation")] + NullFunction { name: String }, + + #[error("a plugin is already loaded into this handle")] + AlreadyLoaded, +} + +pub(crate) struct CustomJet { + pub(crate) name: &'static str, + pub(crate) cmr: Cmr, + pub(crate) source_ty: TypeName, + pub(crate) target_ty: TypeName, + pub(crate) cost: Cost, + pub(crate) code: u64, + pub(crate) code_len: u8, + pub(crate) func: JetFn, +} + +/// A validated plugin: its custom jets plus lookup tables over the base and custom jets. +pub struct LoadedPlugin { + base_set: BaseSet, + base_len: usize, + pub(crate) custom: Vec, + pub(crate) names: HashMap, + pub(crate) trie: DecodeTrie, +} + +impl LoadedPlugin { + /// Validates `desc` as an extension of the base jet set `B`. + /// + /// Strings are copied and leaked, as plugins are never unloaded; `func` pointers stay + /// pointing into the plugin. + /// + /// # Safety + /// + /// `desc` must be null or point to a descriptor whose pointers are valid for the lengths + /// they are paired with, and whose functions follow the ABI for as long as the returned + /// value lives. + pub unsafe fn from_descriptor( + desc: *const PluginDescriptor, + ) -> Result { + let desc = unsafe { desc.as_ref() }.ok_or(LoadError::NullDescriptor)?; + + if desc.abi_version != ABI_VERSION { + return Err(LoadError::AbiVersion { + expected: ABI_VERSION, + found: desc.abi_version, + }); + } + let sys_version = unsafe { desc.simplicity_sys_version.as_slice() }; + if sys_version != SIMPLICITY_SYS_VERSION.as_bytes() { + return Err(LoadError::SimplicitySysVersion { + expected: SIMPLICITY_SYS_VERSION, + found: String::from_utf8_lossy(sys_version).into_owned(), + }); + } + let base_set = BaseSet::try_from(desc.base_set).map_err(LoadError::UnknownBaseSet)?; + if base_set != B::ABI_BASE_SET { + return Err(LoadError::BaseSet { + expected: B::ABI_BASE_SET, + found: base_set, + }); + } + if desc.jets_len > MAX_CUSTOM_JETS { + return Err(LoadError::TooManyJets { + max: MAX_CUSTOM_JETS, + found: desc.jets_len, + }); + } + let descs: &[CustomJetDesc] = if desc.jets_len == 0 { + &[] + } else if desc.jets.is_null() { + return Err(LoadError::NullJets); + } else { + unsafe { std::slice::from_raw_parts(desc.jets, desc.jets_len) } + }; + + let base_len = B::ALL.len(); + let mut names = HashMap::new(); + let mut cmrs = HashMap::new(); + let mut trie = DecodeTrie::new(); + for (index, jet) in B::ALL.iter().enumerate() { + names.insert(jet.to_string(), index); + cmrs.insert(jet.cmr(), jet.to_string()); + trie.insert(encoding(jet), index) + .expect("base jet encodings are prefix-free"); + } + + let mut custom: Vec = Vec::with_capacity(descs.len()); + let mut seen = HashSet::new(); + for (k, jet) in descs.iter().enumerate() { + let name = validate_name(unsafe { jet.name.as_slice() }) + .ok_or(LoadError::InvalidName { index: k })?; + if !seen.insert(name) || names.contains_key(name) { + return Err(LoadError::DuplicateName { name: name.into() }); + } + + let source_ty = validate_type(name, unsafe { jet.source_ty.as_slice() })?; + let target_ty = validate_type(name, unsafe { jet.target_ty.as_slice() })?; + + let cost = u32::try_from(jet.cost_milliweight) + .ok() + .filter(|&cost| cost > 0) + .map(Cost::from_milliweight) + .ok_or(LoadError::InvalidCost { + name: name.into(), + cost: jet.cost_milliweight, + })?; + + if jet.code_len == 0 + || jet.code_len > MAX_CODE_LEN + || (jet.code_len < 64 && jet.code >> jet.code_len != 0) + { + return Err(LoadError::InvalidCode { name: name.into() }); + } + + let func = jet + .func + .ok_or(LoadError::NullFunction { name: name.into() })?; + + // TODO: once the host derives CMRs (see `CustomJetDesc::cmr`), compute it here + // instead of trusting the plugin. + let cmr = Cmr::from_byte_array(jet.cmr); + if let Some(other) = cmrs.insert(cmr, name.to_string()) { + return Err(LoadError::DuplicateCmr { + name: name.into(), + other, + }); + } + + let index = base_len + k; + trie.insert(code_bits(jet.code, jet.code_len), index) + .map_err(|other| LoadError::EncodingConflict { + name: name.into(), + other: match other.checked_sub(base_len) { + None => B::ALL[other].to_string(), + Some(k) => custom[k].name.to_string(), + }, + })?; + + let name: &'static str = Box::leak(name.into()); + names.insert(name.to_string(), index); + custom.push(CustomJet { + name, + cmr, + source_ty, + target_ty, + cost, + code: jet.code, + code_len: jet.code_len, + func, + }); + } + + Ok(Self { + base_set, + base_len, + custom, + names, + trie, + }) + } + + /// The base jet set this plugin extends. + pub fn base_set(&self) -> BaseSet { + self.base_set + } + + pub(crate) fn base_len(&self) -> usize { + self.base_len + } + + /// Names of the custom jets, in descriptor order. + pub fn custom_names(&self) -> impl Iterator + '_ { + self.custom.iter().map(|jet| jet.name) + } +} + +fn encoding(jet: &J) -> Vec { + let mut bytes: Vec = vec![]; + let mut writer = BitWriter::new(&mut bytes as &mut dyn std::io::Write); + let len = jet + .encode(&mut writer) + .expect("writing to a Vec never fails"); + writer.flush_all().expect("writing to a Vec never fails"); + (0..len) + .map(|i| (bytes[i / 8] & (0x80 >> (i % 8))) != 0) + .collect() +} + +fn validate_name(name: &[u8]) -> Option<&str> { + let valid = !name.is_empty() + && name.len() <= MAX_NAME_LEN + && name + .iter() + .all(|&c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == b'_'); + valid.then(|| std::str::from_utf8(name).expect("checked to be ASCII")) +} + +/// Checks `ty` against the `TypeName` grammar, whose parser panics on bad input. +fn validate_type(name: &str, ty: &[u8]) -> Result { + let mut depth = 0usize; + let mut valid = !ty.is_empty() && ty.len() <= MAX_TYPE_NAME_LEN; + for &c in ty.iter().rev() { + match c { + b'1' | b'2' | b'c' | b's' | b'i' | b'l' | b'h' => depth += 1, + b'+' | b'*' if depth >= 2 => depth -= 1, + _ => valid = false, + } + } + if !valid || depth != 1 { + return Err(LoadError::InvalidType { + name: name.into(), + ty: String::from_utf8_lossy(ty).into_owned(), + }); + } + + Ok(TypeName(Box::leak(ty.into()))) +} diff --git a/core/src/plugin/loader.rs b/core/src/plugin/loader.rs new file mode 100644 index 0000000..e13aac5 --- /dev/null +++ b/core/src/plugin/loader.rs @@ -0,0 +1,35 @@ +use std::path::Path; + +use dlopen2::raw::Library; +use jet_plugin_abi::{DESCRIPTOR_SYMBOL, DescriptorFn}; +use simplicity::hashes::sha256; + +use super::env::PluginBase; +use super::loaded::{LoadError, LoadedPlugin}; + +/// Opens the plugin at `path` and validates it as an extension of `B`. +/// +/// The library is never closed: custom jet functions point into it. +pub(crate) fn load( + path: &Path, + sha256: Option<[u8; 32]>, +) -> Result { + if let Some(expected) = sha256 { + // TODO: the file can still change between hashing and opening; open from the hashed + // bytes (e.g. a memfd) if plugins ever stop being operator-trusted. + let found = sha256::Hash::hash(&std::fs::read(path)?).to_byte_array(); + if found != expected { + return Err(LoadError::HashMismatch { + expected: hex::encode(expected), + found: hex::encode(found), + }); + } + } + + let library = Library::open(path)?; + let describe: DescriptorFn = unsafe { library.symbol(DESCRIPTOR_SYMBOL) }?; + // SAFETY: plugins are operator-trusted to follow the ABI. + let plugin = unsafe { LoadedPlugin::from_descriptor::(describe()) }?; + std::mem::forget(library); + Ok(plugin) +} diff --git a/core/src/plugin/mod.rs b/core/src/plugin/mod.rs new file mode 100644 index 0000000..c763cb3 --- /dev/null +++ b/core/src/plugin/mod.rs @@ -0,0 +1,95 @@ +//! Custom jets loaded at runtime from dynamic libraries. +//! +//! A plugin extends one base jet set ([`CoreExtension`] or [`ElementsExtension`]) with custom +//! jets described by a [`jet_plugin_abi::PluginDescriptor`]. The host owns all Simplicity-facing +//! types: [`PluginJet`] and [`PluginEnv`] implement `Jet` and `JetEnvironment` for a plugin +//! declared with [`declare_plugin!`], base jets keep running through the host's own +//! implementation, and custom jets are called through the C ABI. +//! +//! [`CoreExtension`]: crate::jets::bitcoin::CoreExtension +//! [`ElementsExtension`]: crate::jets::elements::ElementsExtension +mod decode; +mod env; +mod jet; +mod loaded; +mod loader; +#[cfg(test)] +mod tests; +mod trampolines; + +use std::fmt::Debug; +use std::hash::Hash; +use std::path::Path; +use std::sync::OnceLock; + +pub use env::{PluginBase, PluginEnv}; +pub use jet::PluginJet; +pub use loaded::{LoadError, LoadedPlugin, MAX_CUSTOM_JETS}; + +/// A plugin handle declared with [`declare_plugin!`]. +/// +/// Each handle holds at most one plugin for the lifetime of the process; plugins are never +/// unloaded. +pub trait PluginTrait: Copy + Ord + Hash + Debug + Send + Sync + 'static { + /// The base jet set the plugin extends. + type Base: PluginBase; + + #[doc(hidden)] + fn cell() -> &'static OnceLock; + + /// The loaded plugin, if any. + fn get() -> Option<&'static LoadedPlugin> { + Self::cell().get() + } + + /// Loads the dynamic library at `path` into this handle. + /// + /// When `sha256` is given, the file must hash to it; `None` skips the check and is meant for + /// development only. + fn load(path: impl AsRef, sha256: Option<[u8; 32]>) -> Result<(), LoadError> { + if Self::get().is_some() { + return Err(LoadError::AlreadyLoaded); + } + let plugin = loader::load::(path.as_ref(), sha256)?; + Self::install(plugin) + } + + /// Installs an already validated plugin into this handle. + fn install(plugin: LoadedPlugin) -> Result<(), LoadError> { + let expected = ::ABI_BASE_SET; + if plugin.base_set() != expected { + return Err(LoadError::BaseSet { + expected, + found: plugin.base_set(), + }); + } + Self::cell() + .set(plugin) + .map_err(|_| LoadError::AlreadyLoaded) + } +} + +/// Declares a plugin handle type extending the given base jet set. +/// +/// ```ignore +/// declare_plugin!(pub Foo: ElementsExtension); +/// Foo::load("libfoo.so", Some(sha256))?; +/// let program = Program::>::from_str(program, witness)?; +/// ``` +#[macro_export] +macro_rules! declare_plugin { + ($vis:vis $name:ident : $base:ty) => { + #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] + $vis struct $name; + + impl $crate::plugin::PluginTrait for $name { + type Base = $base; + + fn cell() -> &'static ::std::sync::OnceLock<$crate::plugin::LoadedPlugin> { + static CELL: ::std::sync::OnceLock<$crate::plugin::LoadedPlugin> = + ::std::sync::OnceLock::new(); + &CELL + } + } + }; +} diff --git a/core/src/plugin/tests.rs b/core/src/plugin/tests.rs new file mode 100644 index 0000000..49e3810 --- /dev/null +++ b/core/src/plugin/tests.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use simplicity::elements::Script; +use simplicity::jet::Jet; +use simplicity::node::CoreConstructible; +use simplicity::{BitMachine, ConstructNode, Word, types}; + +use super::{LoadError, PluginEnv, PluginJet, PluginTrait}; +use crate::jets::elements::ElementsExtension; +use crate::jets::environments::UnchainedEnv; +use crate::jets::tests::dummy_elements_env; + +/// `OP_2 <33-byte key> <33-byte key> OP_2 OP_CHECKMULTISIG`, 71 bytes. +const SCRIPT: &str = "5221033523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf21025eb4655feae15a304653e27441ca8e8ced2bef89c22ab6b20424b4c07b3d14cc52ae"; + +/// `plugin_tests/script_jets`, built next to this test binary as a dev-dependency. +fn script_jets_path() -> std::path::PathBuf { + let exe = std::env::current_exe().unwrap(); + exe.with_file_name(format!( + "{}script_jets{}", + std::env::consts::DLL_PREFIX, + std::env::consts::DLL_SUFFIX + )) +} + +#[test] +fn loads_dynamic_library() { + crate::declare_plugin!(ScriptJets: ElementsExtension); + crate::declare_plugin!(Unused: ElementsExtension); + + let path = script_jets_path(); + let sha256 = + simplicity::hashes::sha256::Hash::hash(&std::fs::read(&path).unwrap()).to_byte_array(); + + assert!(matches!( + Unused::load(&path, Some([0; 32])), + Err(LoadError::HashMismatch { .. }) + )); + assert!(matches!( + Unused::load(path.with_file_name("missing.so"), None), + Err(LoadError::Io(_)) | Err(LoadError::Open(_)) + )); + assert!(Unused::get().is_none()); + + ScriptJets::load(&path, Some(sha256)).unwrap(); + let plugin = ScriptJets::get().unwrap(); + assert_eq!( + plugin.custom_names().collect::>(), + ["script_len", "opcode_at"] + ); + + let redeem = types::Context::with_context(|ctx| { + let jet = + |name| Arc::::jet(&ctx, &PluginJet::::parse(name).unwrap()); + let actual = jet("script_len"); + let expected = Arc::::const_word(&ctx, Word::u16(71)); + let pair = Arc::::pair(&actual, &expected).unwrap(); + let check = Arc::::comp(&pair, &jet("eq_16")).unwrap(); + Arc::::comp(&check, &jet("verify")) + .unwrap() + .finalize_unpruned() + .unwrap() + }); + let env = PluginEnv::::new(UnchainedEnv::new( + Script::from_hex_no_prefix(SCRIPT).unwrap(), + dummy_elements_env(), + )); + let mut mac = BitMachine::for_program(&redeem).unwrap(); + mac.exec(&redeem, &env).unwrap(); +} diff --git a/core/src/plugin/trampolines.rs b/core/src/plugin/trampolines.rs new file mode 100644 index 0000000..bdccc6a --- /dev/null +++ b/core/src/plugin/trampolines.rs @@ -0,0 +1,58 @@ +//! One trampoline per custom jet position, see `PluginEnv::c_jet_ptr`. +// +// TODO: generate the table with a proc macro if plugins need more than 32 custom jets. +use super::MAX_CUSTOM_JETS; +use super::env::PluginEnv; +use crate::jets::base::JetFn; +use simplicity::ffi::CFrameItem; + +/// Runs custom jet `IDX` of `Plugin`. +fn custom_jet_slot( + dst: &mut CFrameItem, + src: CFrameItem, + env: &PluginEnv, +) -> bool { + match Plugin::get().and_then(|plugin| plugin.custom.get(IDX)) { + Some(jet) => env.call(jet, dst, src), + None => false, + } +} + +pub(super) struct CustomJetSlots(Plugin); + +impl CustomJetSlots { + pub(super) const TABLE: [JetFn>; MAX_CUSTOM_JETS] = [ + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + custom_jet_slot::, + ]; +} diff --git a/core/src/policy.rs b/core/src/policy.rs new file mode 100644 index 0000000..4beb6f4 --- /dev/null +++ b/core/src/policy.rs @@ -0,0 +1,184 @@ +//! Opt-in Bitcoin policy preparation and bounded execution. +//! +//! Programs and witnesses are binary here. There is no legacy decoder fallback. + +use std::sync::Arc; + +use simplicity::bit_machine::{ExecutionError, LimitError}; +use simplicity::bitcoin::{self, XOnlyPublicKey, psbt::Psbt, taproot::TapNodeHash}; +use simplicity::{BitIter, BitMachine, Cmr, CommitNode, Cost, DecodeError, RedeemNode}; + +use crate::jets::policy::{PolicyEnv, PolicyJet}; +use crate::runner::MAX_EXECUTION_COST; + +pub use simplicity_unchained_policy::{Builtin, Context, ContextSummary}; + +#[derive(Debug, thiserror::Error)] +pub enum PolicyError { + #[error("invalid policy program or witness: {0}")] + Decode(#[source] DecodeError), + #[error("invalid policy context: {0}")] + Context(#[from] simplicity_unchained_policy::Error), + #[error("witnessed program CMR differs from the prepared policy")] + CommitmentMismatch, + #[error("policy rejected: {0}")] + PolicyRejected(#[source] ExecutionError), + #[error("program execution cost {cost} exceeds maximum {max} milliweight")] + ExecutionCostExceeded { cost: Cost, max: Cost }, + #[error("bit machine memory limit exceeded: {0}")] + MemoryLimit(#[source] LimitError), + #[error("internal policy VM error: {0}")] + Vm(#[source] ExecutionError), +} + +#[derive(Debug)] +pub struct EvaluationResult { + pub cmr: Cmr, + pub context: ContextSummary, + pub cost_milliweight: u32, + /// Static upper bound, not a wall-clock measurement or consensus calibration. + pub bounded_cost: Cost, +} + +pub struct PreparedPolicy { + program: Arc<[u8]>, + commitment: Arc, + env: PolicyEnv, +} + +/// Decode the original commitment without requiring any approval witness. +pub fn policy_cmr(program_bytes: &[u8]) -> Result { + decode_commitment(program_bytes).map(|program| program.cmr()) +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_policy( + program_bytes: &[u8], + psbt: &Psbt, + input_index: usize, + script: &[u8], + network: bitcoin::Network, + base_service_xonly: XOnlyPublicKey, + user_xonly: XOnlyPublicKey, + recovery: TapNodeHash, +) -> Result { + PreparedPolicy::new( + program_bytes, + psbt, + input_index, + script, + network, + base_service_xonly, + user_xonly, + recovery, + ) +} + +fn decode_commitment(program_bytes: &[u8]) -> Result, PolicyError> { + CommitNode::decode::<_, PolicyJet>(BitIter::from(program_bytes)).map_err(PolicyError::Decode) +} + +impl PreparedPolicy { + #[allow(clippy::too_many_arguments)] + pub fn new( + program_bytes: &[u8], + psbt: &Psbt, + input_index: usize, + script: &[u8], + network: bitcoin::Network, + base_service_xonly: XOnlyPublicKey, + user_xonly: XOnlyPublicKey, + recovery: TapNodeHash, + ) -> Result { + let commitment = decode_commitment(program_bytes)?; + let context = Context::new( + psbt, + input_index, + commitment.cmr().to_byte_array(), + script, + network, + base_service_xonly, + user_xonly, + recovery, + )?; + Ok(Self { + program: Arc::from(program_bytes), + commitment, + env: PolicyEnv::new(context), + }) + } + + pub fn context(&self) -> &Context { + self.env.context() + } + + pub fn summary(&self) -> ContextSummary { + self.context().summary() + } + + pub fn cmr(&self) -> Cmr { + self.commitment.cmr() + } + + pub fn evaluate_default(&self, witness_bytes: &[u8]) -> Result { + self.evaluate(witness_bytes, MAX_EXECUTION_COST) + } + + pub fn evaluate_milliweight( + &self, + witness_bytes: &[u8], + max_milliweight: u32, + ) -> Result { + self.evaluate(witness_bytes, Cost::from_milliweight(max_milliweight)) + } + + /// A caller may lower, but never raise, the runner's default execution cap. + pub fn evaluate( + &self, + witness_bytes: &[u8], + limit: Cost, + ) -> Result { + let program = RedeemNode::decode::<_, _, PolicyJet>( + BitIter::from(self.program.as_ref()), + BitIter::from(witness_bytes), + ) + .map_err(PolicyError::Decode)?; + if program.cmr() != self.cmr() { + return Err(PolicyError::CommitmentMismatch); + } + let mut machine = bounded_machine(&program, limit)?; + machine + .exec(&program, &self.env) + .map_err(|error| match error { + ExecutionError::ReachedFailNode(..) + | ExecutionError::ReachedPrunedBranch(..) + | ExecutionError::JetFailed(..) => PolicyError::PolicyRejected(error), + ExecutionError::LimitExceeded(limit) => PolicyError::MemoryLimit(limit), + _ => PolicyError::Vm(error), + })?; + Ok(EvaluationResult { + cmr: self.cmr(), + context: self.summary(), + // Simplicity 0.9 exposes Cost's exact milliweight only through Display. + cost_milliweight: program + .bounds() + .cost + .to_string() + .parse() + .expect("Cost displays its u32 milliweight"), + bounded_cost: program.bounds().cost, + }) + } +} + +fn bounded_machine(program: &RedeemNode, limit: Cost) -> Result { + let max = limit.min(MAX_EXECUTION_COST); + let cost = program.bounds().cost; + if cost > max { + return Err(PolicyError::ExecutionCostExceeded { cost, max }); + } + BitMachine::for_program(program).map_err(PolicyError::MemoryLimit) +} + +#[cfg(test)] +pub(crate) mod tests; diff --git a/core/src/policy/tests.rs b/core/src/policy/tests.rs new file mode 100644 index 0000000..cbe8404 --- /dev/null +++ b/core/src/policy/tests.rs @@ -0,0 +1,348 @@ +use super::*; +use simplicity::bitcoin::hashes::Hash; +use simplicity::bitcoin::secp256k1::{Keypair, Message, Secp256k1, SecretKey}; +use simplicity::bitcoin::{ + Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, absolute, transaction, +}; +use simplicity::node::{CoreConstructible, WitnessConstructible}; +use simplicity::{ConstructNode, Word, jet::Core, types}; + +pub(crate) struct Fixture { + pub psbt: Psbt, + pub script: Vec, + pub network: bitcoin::Network, + pub base: XOnlyPublicKey, + pub user: XOnlyPublicKey, + pub recovery: TapNodeHash, +} + +impl Fixture { + pub fn new(program: &[u8], inputs: usize, outputs: usize, max_scripts: bool) -> Self { + Self::new_on_network( + program, + inputs, + outputs, + max_scripts, + bitcoin::Network::Testnet, + ) + } + + pub fn new_on_network( + program: &[u8], + inputs: usize, + outputs: usize, + max_scripts: bool, + network: bitcoin::Network, + ) -> Self { + let secp = Secp256k1::new(); + let key = |byte| { + Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[byte; 32]).unwrap()) + .x_only_public_key() + .0 + }; + let base = key(1); + let user = key(2); + let recovery = TapNodeHash::from_byte_array([3; 32]); + // Include a compressed pubkey for both legacy script-reader adapters. + let mut script = vec![0x21, 0x02]; + script.extend_from_slice(&user.serialize()); + let cmr = policy_cmr(program).unwrap(); + let service = simplicity_unchained_policy::tweak_public_key( + base, + cmr.to_byte_array(), + &script, + network, + ) + .unwrap(); + let full = |key| { + bitcoin::PublicKey::new(bitcoin::secp256k1::PublicKey::from_x_only_public_key( + key, + bitcoin::secp256k1::Parity::Even, + )) + }; + let (address, _) = + crate::utils::generate_p2tr_address_btc(&full(service), &full(user), recovery, network) + .unwrap(); + let parent = Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::ZERO, + input: vec![TxIn::default()], + output: vec![ + TxOut { + value: Amount::from_sat(100_000), + script_pubkey: address.script_pubkey(), + }; + inputs + ], + }; + let tx = Transaction { + version: transaction::Version::TWO, + lock_time: absolute::LockTime::from_consensus(500), + input: (0..inputs) + .map(|vout| TxIn { + previous_output: OutPoint { + txid: parent.compute_txid(), + vout: vout as u32, + }, + sequence: Sequence::from_consensus(10), + ..TxIn::default() + }) + .collect(), + output: vec![ + TxOut { + value: Amount::from_sat(90_000 * inputs as u64 / outputs as u64), + script_pubkey: if max_scripts { + ScriptBuf::from_bytes(vec![0x51; 10_000]) + } else { + address.script_pubkey() + }, + }; + outputs + ], + }; + let mut psbt = Psbt::from_unsigned_tx(tx).unwrap(); + for (index, input) in psbt.inputs.iter_mut().enumerate() { + input.non_witness_utxo = Some(parent.clone()); + input.witness_utxo = Some(parent.output[index].clone()); + } + Self { + psbt, + script, + network, + base, + user, + recovery, + } + } + + pub fn prepare(&self, program: &[u8], index: usize) -> Result { + prepare_policy( + program, + &self.psbt, + index, + &self.script, + self.network, + self.base, + self.user, + self.recovery, + ) + } +} + +pub(crate) fn unit_program() -> Vec { + types::Context::with_context(|ctx| { + Arc::::unit(&ctx) + .finalize_types() + .unwrap() + .to_vec_without_witness() + }) +} + +fn approval_program(approver: XOnlyPublicKey) -> Vec { + types::Context::with_context(|ctx| { + let pk = Arc::::const_word(&ctx, Word::u256(approver.serialize())); + let message = Arc::::jet( + &ctx, + &PolicyJet::Builtin(Builtin::PolicyTransactionApprovalHash), + ); + let pk_message = Arc::::pair(&pk, &message).unwrap(); + let signature = Arc::::witness(&ctx, None); + let arguments = Arc::::pair(&pk_message, &signature).unwrap(); + let verify = Arc::::jet( + &ctx, + &PolicyJet::Legacy(crate::jets::bitcoin::CoreExtension::Core( + Core::Bip0340Verify, + )), + ); + Arc::::comp(&arguments, &verify) + .unwrap() + .finalize_types() + .unwrap() + .to_vec_without_witness() + }) +} + +#[test] +fn prepare_approval_without_witness_then_execute_exact_approved_context() { + let secp = Secp256k1::new(); + let approver = Keypair::from_secret_key(&secp, &SecretKey::from_slice(&[7; 32]).unwrap()); + let program = approval_program(approver.x_only_public_key().0); + let fixture = Fixture::new(&program, 2, 2, false); + let prepared = fixture.prepare(&program, 0).unwrap(); + assert_eq!(policy_cmr(&program).unwrap(), prepared.cmr()); + assert_eq!(prepared.context().cmr(), prepared.cmr().to_byte_array()); + assert!(matches!( + prepared.evaluate_default(&[]), + Err(PolicyError::Decode(_)) + )); + let digest = prepared.context().policy_transaction_approval_hash(); + let signature = secp.sign_schnorr_no_aux_rand(&Message::from_digest(digest), &approver); + let result = prepared.evaluate_default(signature.as_ref()).unwrap(); + assert_eq!(result.cmr, prepared.cmr()); + assert!(result.cost_milliweight > 10_000); + assert_eq!( + result.context.policy_transaction_approval_hash, + prepared.summary().policy_transaction_approval_hash + ); + let mut invalid = signature.as_ref().to_vec(); + invalid[0] ^= 1; + assert!(matches!( + prepared.evaluate_default(&invalid), + Err(PolicyError::PolicyRejected(_)) + )); + let mut extra = signature.as_ref().to_vec(); + extra.push(0); + assert!(matches!( + prepared.evaluate_default(&extra), + Err(PolicyError::Decode(_)) + )); + let second = fixture.prepare(&program, 1).unwrap(); + assert_eq!(digest, second.context().policy_transaction_approval_hash()); + assert_ne!( + prepared.context().policy_approval_hash(), + second.context().policy_approval_hash() + ); + second.evaluate_default(signature.as_ref()).unwrap(); + let mut changed = fixture; + changed.psbt.unsigned_tx.output[0].value = Amount::from_sat(89_999); + let changed = changed.prepare(&program, 0).unwrap(); + assert!(matches!( + changed.evaluate_default(signature.as_ref()), + Err(PolicyError::PolicyRejected(_)) + )); +} + +#[test] +fn prepared_context_is_immutable_and_preserves_legacy_approval_semantics() { + let program = unit_program(); + for (network, legacy_network) in [ + (bitcoin::Network::Bitcoin, crate::BitcoinNetwork::Mainnet), + (bitcoin::Network::Testnet, crate::BitcoinNetwork::Testnet), + (bitcoin::Network::Testnet4, crate::BitcoinNetwork::Testnet4), + ] { + let fixture = Fixture::new_on_network(&program, 2, 2, false, network); + for index in 0..2 { + let prepared = fixture.prepare(&program, index).unwrap(); + let expected = crate::runner::bitcoin_execution_environment( + &fixture.psbt, + index, + prepared.cmr(), + &simplicity::elements::Script::from(fixture.script.clone()), + legacy_network, + ) + .unwrap(); + assert_eq!( + prepared.context().legacy_approval_hash(), + expected.approval_hash + ); + assert_ne!( + prepared.context().signing_commitment(), + crate::utils::signing_commitment(prepared.cmr(), &fixture.script), + ); + } + } + let mut fixture = Fixture::new(&program, 2, 2, false); + let prepared = fixture.prepare(&program, 1).unwrap(); + let digest = prepared.context().cooperative_sighash_default(); + fixture.psbt.unsigned_tx.output[0].value = Amount::from_sat(1); + fixture.psbt.inputs[0].witness_utxo = None; + prepared.evaluate_default(&[]).unwrap(); + assert_eq!(digest, prepared.context().cooperative_sighash_default()); +} + +#[test] +fn cost_boundary_and_preallocation_rejection_are_exact() { + assert_eq!( + MAX_EXECUTION_COST, + Cost::from_milliweight(simplicity_unchained_policy::MAX_EXECUTION_COST_MILLIWEIGHT), + ); + let program = unit_program(); + let fixture = Fixture::new(&program, 1, 1, false); + let prepared = fixture.prepare(&program, 0).unwrap(); + let result = prepared.evaluate_default(&[]).unwrap(); + prepared + .evaluate(&[], result.bounded_cost) + .expect("exact limit is inclusive"); + assert!(matches!( + prepared.evaluate_milliweight(&[], result.cost_milliweight - 1), + Err(PolicyError::ExecutionCostExceeded { .. }) + )); + for levels in [20, 60] { + let expensive = types::Context::with_context(|ctx| { + let mut node = Arc::::unit(&ctx); + for _ in 0..levels { + node = Arc::::comp(&node, &node).unwrap(); + } + node.finalize_types().unwrap().to_vec_without_witness() + }); + assert!(expensive.len() < 256); + let fixture = Fixture::new(&expensive, 1, 1, false); + let prepared = fixture.prepare(&expensive, 0).unwrap(); + let redeem = RedeemNode::decode::<_, _, PolicyJet>( + BitIter::from(expensive.as_slice()), + BitIter::from(&[][..]), + ) + .unwrap(); + assert!(BitMachine::for_program(&redeem).is_ok()); + assert!(redeem.bounds().cost > MAX_EXECUTION_COST); + assert!(matches!( + prepared.evaluate(&[], Cost::from_milliweight(u32::MAX)), + Err(PolicyError::ExecutionCostExceeded { + max: MAX_EXECUTION_COST, + .. + }) + )); + } +} + +#[test] +fn malformed_program_and_invalid_context_remain_distinct() { + assert!(matches!(policy_cmr(&[]), Err(PolicyError::Decode(_)))); + let program = unit_program(); + let mut fixture = Fixture::new(&program, 1, 1, false); + fixture.psbt.inputs[0].non_witness_utxo = None; + assert!(matches!( + fixture.prepare(&program, 0), + Err(PolicyError::Context( + simplicity_unchained_policy::Error::MissingParent(0) + )) + )); +} + +#[test] +fn new_profile_program_is_not_interpreted_as_legacy() { + let program = types::Context::with_context(|ctx| { + let current = Arc::::jet(&ctx, &PolicyJet::Builtin(Builtin::CurrentIndex)); + let expected = Arc::::const_word(&ctx, Word::u32(0)); + let pair = Arc::::pair(¤t, &expected).unwrap(); + let equal = Arc::::jet( + &ctx, + &PolicyJet::Legacy(crate::jets::bitcoin::CoreExtension::Core(Core::Eq32)), + ); + let check = Arc::::comp(&pair, &equal).unwrap(); + let verify = Arc::::jet( + &ctx, + &PolicyJet::Legacy(crate::jets::bitcoin::CoreExtension::Core(Core::Verify)), + ); + Arc::::comp(&check, &verify) + .unwrap() + .finalize_types() + .unwrap() + .to_vec_without_witness() + }); + let fixture = Fixture::new(&program, 2, 2, false); + fixture + .prepare(&program, 0) + .unwrap() + .evaluate_default(&[]) + .unwrap(); + assert!(matches!( + fixture.prepare(&program, 1).unwrap().evaluate_default(&[]), + Err(PolicyError::PolicyRejected(_)) + )); + if let Ok(legacy) = CommitNode::decode::<_, crate::jets::bitcoin::CoreExtension>(BitIter::from( + program.as_slice(), + )) { + assert_ne!(legacy.cmr(), policy_cmr(&program).unwrap()); + } +} diff --git a/core/src/program.rs b/core/src/program.rs new file mode 100644 index 0000000..ff7e5a8 --- /dev/null +++ b/core/src/program.rs @@ -0,0 +1,69 @@ +use std::marker::PhantomData; +use std::sync::Arc; + +use simplicity::base64::prelude::{BASE64_STANDARD, Engine as _}; +use simplicity::jet::Jet; +use simplicity::{BitIter, Cmr, CommitNode, ParseError, RedeemNode}; + +/// A Simplicity program decoded with jets of type `J`. +/// +/// Port of `hal_simplicity::Program`. +pub struct Program { + commit_prog: Arc, + redeem_prog: Option>, + _jet: PhantomData J>, +} + +impl Program { + /// Decodes a program and an optional witness, each given as hex or base64. + pub fn from_str(prog: &str, witness: Option<&str>) -> Result { + let prog_bytes = hex_or_base64(prog)?; + let commit_prog = CommitNode::decode::<_, J>(BitIter::new(prog_bytes.iter().copied())) + .map_err(ParseError::Decode)?; + + let redeem_prog = witness + .map(|witness| { + let wit_bytes = hex_or_base64(witness)?; + RedeemNode::decode::<_, _, J>( + BitIter::new(prog_bytes.into_iter()), + BitIter::new(wit_bytes.into_iter()), + ) + .map_err(ParseError::Decode) + }) + .transpose()?; + + Ok(Self { + commit_prog, + redeem_prog, + _jet: PhantomData, + }) + } + + /// The CMR of the program. + pub fn cmr(&self) -> Cmr { + self.commit_prog.cmr() + } + + /// The commitment-time program. + pub fn commit_prog(&self) -> &CommitNode { + &self.commit_prog + } + + /// The redemption-time program, if a witness was given. + pub fn redeem_node(&self) -> Option<&Arc> { + self.redeem_prog.as_ref() + } +} + +/// Same detection as `hal_simplicity::hex_or_base64`, kept so that existing inputs decode to the +/// same programs: hex only when every char is a lowercase hex letter, base64 otherwise. +fn hex_or_base64(s: &str) -> Result, ParseError> { + if s.len().is_multiple_of(2) + && s.bytes() + .all(|b| b.is_ascii_hexdigit() && b.is_ascii_lowercase()) + { + simplicity::hex::decode_to_vec(s).map_err(ParseError::Hex) + } else { + BASE64_STANDARD.decode(s).map_err(ParseError::Base64) + } +} diff --git a/core/src/runner.rs b/core/src/runner.rs index 3e36b35..d535eb8 100644 --- a/core/src/runner.rs +++ b/core/src/runner.rs @@ -1,15 +1,15 @@ use std::sync::Arc; -use hal_simplicity::hal_simplicity::Program; -use hal_simplicity::simplicity::BitMachine; -use hal_simplicity::simplicity::bit_machine::ExecutionError; -use hal_simplicity::simplicity::bitcoin::psbt; -use hal_simplicity::simplicity::elements::taproot::{LeafVersion, TaprootMerkleBranch}; -use hal_simplicity::simplicity::elements::{BlockHash, Script}; -use hal_simplicity::simplicity::elements::{Transaction, pset, schnorr::UntweakedPublicKey}; -use hal_simplicity::simplicity::{ +use simplicity::bit_machine::ExecutionError; +use simplicity::bitcoin::psbt; +use simplicity::elements::taproot::{LeafVersion, TaprootMerkleBranch}; +use simplicity::elements::{BlockHash, Script}; +use simplicity::elements::{Transaction, pset, schnorr::UntweakedPublicKey}; +use simplicity::{BitMachine, Cost, RedeemNode}; +use simplicity::{ Cmr, elements::taproot::ControlBlock, + jet::CoreEnv, jet::elements::{ElementsEnv, ElementsUtxo}, }; @@ -17,36 +17,47 @@ use hex_literal::hex; use crate::jets::bitcoin::CoreExtension; use crate::jets::elements::ElementsExtension; -use crate::jets::environments::UnchainedEnv; +use crate::jets::environments::{BitcoinTxEnv, UnchainedEnv}; +use crate::program::Program; +use crate::utils::signing_commitment; use crate::{BitcoinNetwork, ElementsNetwork}; pub struct SimplicityRunner; +/// Maximum execution cost accepted by the off-chain runner, in milliweight. +pub const MAX_EXECUTION_COST: Cost = Cost::from_milliweight(10_000_000); + #[derive(Debug, thiserror::Error)] pub enum RunnerError { - #[error("input index {index} out-of-range for PSET with {total} inputs")] + #[error("input index {index} out-of-range for transaction with {total} inputs")] InputIndexOutOfRange { index: usize, total: usize }, - #[error("failed to parse genesis hash: {0}")] - GenesisHashParse(hal_simplicity::simplicity::elements::hashes::hex::HexToArrayError), + #[error("PSBT input count {psbt_inputs} does not match transaction input count {tx_inputs}")] + PsbtInputCountMismatch { + psbt_inputs: usize, + tx_inputs: usize, + }, #[error("could not find Simplicity leaf in PSET taptree with CMR {cmr})")] MissingSimplicityLeaf { cmr: String }, #[error("failed to extract transaction from PSET: {0}")] - PsetExtract(hal_simplicity::simplicity::elements::pset::Error), + PsetExtract(simplicity::elements::pset::Error), #[error("witness_utxo field not populated for input {0}")] MissingWitnessUtxo(usize), #[error("invalid program: {0}")] - ProgramParse(hal_simplicity::simplicity::ParseError), + ProgramParse(simplicity::ParseError), #[error("program does not have a redeem node")] NoRedeemNode, + #[error("program execution cost {cost} exceeds maximum {max} milliweight")] + ExecutionCostExceeded { cost: Cost, max: Cost }, + #[error("failed to construct bit machine: {0}")] - BitMachineConstruction(hal_simplicity::simplicity::bit_machine::LimitError), + BitMachineConstruction(simplicity::bit_machine::LimitError), #[error("execution error: {0}")] ExecutionError(ExecutionError), @@ -64,19 +75,13 @@ impl SimplicityRunner { let program = Program::::from_str(program, witness) .map_err(RunnerError::ProgramParse)?; - let elements_env = elements_execution_environment( - &pset, - input_idx, - program.cmr(), - network.genesis_hash(), - )?; - - let env = UnchainedEnv::new(redeem_script, elements_env); - let redeem_node = program.redeem_node().ok_or(RunnerError::NoRedeemNode)?; + let mut mac = bounded_machine(redeem_node)?; - let mut mac = - BitMachine::for_program(redeem_node).map_err(RunnerError::BitMachineConstruction)?; + let elements_env = + elements_execution_environment(pset, input_idx, program.cmr(), network.genesis_hash())?; + + let env = UnchainedEnv::new(redeem_script, elements_env); mac.exec(redeem_node, &env) .map_err(RunnerError::ExecutionError)?; @@ -84,24 +89,23 @@ impl SimplicityRunner { Ok(program.commit_prog().cmr()) } - // NOTE: We will need unused parameters later when we will add more jets. pub fn execute_bitcoin( program: &str, witness: Option<&str>, - _input_idx: usize, - _psbt: &psbt::Psbt, + input_idx: usize, + psbt: &psbt::Psbt, redeem_script: Script, - _network: BitcoinNetwork, + network: BitcoinNetwork, ) -> Result { let program = Program::::from_str(program, witness) .map_err(RunnerError::ProgramParse)?; - let env = UnchainedEnv::new(redeem_script, ()); - let redeem_node = program.redeem_node().ok_or(RunnerError::NoRedeemNode)?; + let mut mac = bounded_machine(redeem_node)?; - let mut mac = - BitMachine::for_program(redeem_node).map_err(RunnerError::BitMachineConstruction)?; + let bitcoin_env = + bitcoin_execution_environment(psbt, input_idx, program.cmr(), &redeem_script, network)?; + let env = UnchainedEnv::new(redeem_script, bitcoin_env); mac.exec(redeem_node, &env) .map_err(RunnerError::ExecutionError)?; @@ -110,7 +114,87 @@ impl SimplicityRunner { } } -fn elements_execution_environment( +fn check_execution_cost(cost: Cost) -> Result<(), RunnerError> { + if cost > MAX_EXECUTION_COST { + return Err(RunnerError::ExecutionCostExceeded { + cost, + max: MAX_EXECUTION_COST, + }); + } + Ok(()) +} + +fn bounded_machine(program: &RedeemNode) -> Result { + check_execution_cost(program.bounds().cost)?; + BitMachine::for_program(program).map_err(RunnerError::BitMachineConstruction) +} + +/// Builds the Bitcoin context and `tx_approval_hash` digest for a selected input. +/// +/// The digest commits to the network, input index, program/script commitment, +/// unsigned transaction and all `witness_utxo` values and scripts. It is not a +/// BIP-341 sighash; approvers must independently verify the PSBT's prevouts. +pub fn bitcoin_execution_environment( + psbt: &psbt::Psbt, + input_idx: usize, + cmr: Cmr, + redeem_script: &Script, + network: BitcoinNetwork, +) -> Result { + use simplicity::bitcoin::{ + consensus::serialize, + hashes::{Hash, HashEngine, sha256}, + }; + + let tx_inputs = psbt.unsigned_tx.input.len(); + if psbt.inputs.len() != tx_inputs { + return Err(RunnerError::PsbtInputCountMismatch { + psbt_inputs: psbt.inputs.len(), + tx_inputs, + }); + } + let input_index = u32::try_from(input_idx) + .ok() + .filter(|_| input_idx < tx_inputs) + .ok_or(RunnerError::InputIndexOutOfRange { + index: input_idx, + total: tx_inputs, + })?; + let prevouts = psbt + .inputs + .iter() + .enumerate() + .map(|(index, input)| { + input + .witness_utxo + .clone() + .ok_or(RunnerError::MissingWitnessUtxo(index)) + }) + .collect::, _>>()?; + let network_id = match network { + BitcoinNetwork::Mainnet => 0, + BitcoinNetwork::Testnet => 1, + BitcoinNetwork::Testnet4 => 2, + }; + let tag = sha256::Hash::hash(b"SimplicityUnchained/BitcoinApproval/v1"); + let mut engine = sha256::Hash::engine(); + engine.input(tag.as_byte_array()); + engine.input(tag.as_byte_array()); + engine.input(&[network_id]); + engine.input(&input_index.to_be_bytes()); + engine.input(&signing_commitment(cmr, redeem_script.as_bytes())); + engine.input(sha256::Hash::hash(&serialize(&psbt.unsigned_tx)).as_byte_array()); + engine.input(sha256::Hash::hash(&serialize(&prevouts)).as_byte_array()); + + Ok(BitcoinTxEnv { + tx: psbt.unsigned_tx.clone(), + input_index, + approval_hash: sha256::Hash::from_engine(engine).to_byte_array(), + core_env: CoreEnv::new(), + }) +} + +pub fn elements_execution_environment( pset: &pset::PartiallySignedTransaction, input_idx: usize, cmr: Cmr, @@ -158,44 +242,273 @@ fn dummy_control_block() -> ControlBlock { ControlBlock { leaf_version, internal_key, - output_key_parity: hal_simplicity::simplicity::elements::secp256k1_zkp::Parity::Even, + output_key_parity: simplicity::elements::secp256k1_zkp::Parity::Even, merkle_branch: TaprootMerkleBranch::default(), } } #[cfg(test)] mod tests { - use crate::runner::SimplicityRunner; + use super::*; + use simplicity::{ + ConstructNode, + base64::prelude::{BASE64_STANDARD, Engine as _}, + node::CoreConstructible, + types, + }; + + fn shared_program(levels: usize) -> String { + let bytes = types::Context::with_context(|ctx| { + let mut node = Arc::::unit(&ctx); + for _ in 0..levels { + node = Arc::::comp(&node, &node).unwrap(); + } + node.finalize_types().unwrap().to_vec_without_witness() + }); + assert!(bytes.len() < 256); + BASE64_STANDARD.encode(bytes) + } - use hal_simplicity::simplicity::elements::Script; - use hal_simplicity::simplicity::elements::hex::FromHex; - use hex_literal::hex; + #[test] + fn execution_cost_boundary() { + assert!(check_execution_cost(MAX_EXECUTION_COST).is_ok()); + assert!(matches!( + check_execution_cost(MAX_EXECUTION_COST + Cost::from_milliweight(1)), + Err(RunnerError::ExecutionCostExceeded { .. }) + )); + assert!(check_execution_cost(Cost::from_milliweight(u32::MAX)).is_err()); + } - use hal_simplicity::simplicity::elements::encode::deserialize; - use hal_simplicity::simplicity::elements::pset::PartiallySignedTransaction; + #[test] + fn both_runners_reject_expensive_shared_dags() { + let tx = simplicity::bitcoin::Transaction { + version: simplicity::bitcoin::transaction::Version::TWO, + lock_time: simplicity::bitcoin::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + let psbt = psbt::Psbt::from_unsigned_tx(tx).unwrap(); + let pset = pset::PartiallySignedTransaction::new_v2(); + for levels in [20, 60] { + let encoded = shared_program(levels); + let program = Program::::from_str(&encoded, Some("")).unwrap(); + let redeem = program.redeem_node().unwrap(); + // Memory checks alone accept these tiny, exponentially costly DAGs. + assert!(BitMachine::for_program(redeem).is_ok()); + assert!(redeem.bounds().cost > MAX_EXECUTION_COST); + if levels == 20 { + assert!(redeem.bounds().cost.is_consensus_valid()); + } + assert!(matches!( + SimplicityRunner::execute_bitcoin( + &encoded, + Some(""), + 0, + &psbt, + Script::new(), + BitcoinNetwork::Testnet, + ), + Err(RunnerError::ExecutionCostExceeded { .. }) + )); + assert!(matches!( + SimplicityRunner::execute_elements( + &encoded, + Some(""), + 0, + &pset, + Script::new(), + ElementsNetwork::LiquidTestnet, + ), + Err(RunnerError::ExecutionCostExceeded { .. }) + )); + } + } #[test] - fn it_works() { - let script = Script::from_hex( - "5221033523982d58e94be3b735731593f8225043880d53727235b566c515d24a0f7baf21025eb4655feae15a304653e27441ca8e8ced2bef89c22ab6b20424b4c07b3d14cc52ae" - ).unwrap(); + fn inexpensive_program_still_executes() { + let encoded = shared_program(3); + let program = Program::::from_str(&encoded, Some("")).unwrap(); + let redeem = program.redeem_node().unwrap(); + let mut machine = bounded_machine(redeem).unwrap(); + let env = bitcoin_execution_environment( + &approval_psbt(), + 0, + program.cmr(), + &Script::new(), + BitcoinNetwork::Testnet, + ) + .unwrap(); + machine + .exec(redeem, &UnchainedEnv::new(Script::new(), env)) + .unwrap(); + } - let program = "4gTaj1eRafl6Ylk5SOxn0YFNK1owqziBW1okfwoUbyI60plzg9+aftH+iEF0HPfdhmi6u3/nCaFpYYsjIjrE6oXS8IEIFCbTPw2hAxCY+ss0X9VBirWgK0d+njZWFwRZorrYK2HgFBAIQKE2ACCP8CEChBhocKkGHHCxbGAgG0DgoHCw"; + fn approval_psbt() -> psbt::Psbt { + use simplicity::bitcoin::{self, Amount, TxIn, TxOut}; + let mut inputs = vec![TxIn::default(), TxIn::default()]; + inputs[0].previous_output.vout = 0; + inputs[1].previous_output.vout = 1; + let mut psbt = psbt::Psbt::from_unsigned_tx(bitcoin::Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: inputs, + output: vec![TxOut { + value: Amount::from_sat(50_000), + script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]), + }], + }) + .unwrap(); + for input in &mut psbt.inputs { + input.witness_utxo = Some(TxOut { + value: Amount::from_sat(100_000), + script_pubkey: bitcoin::ScriptBuf::from_bytes(vec![0x51]), + }); + } + psbt + } - let pset_bytes = hex!( - "70736574ff0102040200000001030400000000010401010105010201fb04020000000001014e01499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c140100000000000186a000220020649be4e4f326f85b2187adb0698d9cab59c4b1c747cc6d884211e90e60484cf001070001080100010e201b42ba45a12d33a9efd32b738e603f5c606dcd59353fc5826f7486d4d5459858010f0400000000011004ffffffff00010308b88201000000000007fc04707365740220499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c140104220020649be4e4f326f85b2187adb0698d9cab59c4b1c747cc6d884211e90e60484cf000010308e80300000000000007fc04707365740220499a818545f6bae39fc03b637f2a4e1e64e590cac1bc3a6f6d71aa4443654c1401040000" + #[test] + fn approval_hash_binds_transaction_prevouts_input_network_and_policy() { + use simplicity::bitcoin::{self, Amount, Sequence}; + let psbt = approval_psbt(); + let cmr = Cmr::from_byte_array([1; 32]); + let script = Script::from(vec![0x52]); + let hash = |psbt: &psbt::Psbt| { + bitcoin_execution_environment(psbt, 0, cmr, &script, BitcoinNetwork::Testnet) + .unwrap() + .approval_hash + }; + let original = hash(&psbt); + assert_eq!( + hex::encode(original), + "1b05942d9d96d9625b39f626593058f550e999463bd361f77512af00a7a20a8d", + ); + let mut variants = Vec::new(); + let mut changed = psbt.clone(); + changed.unsigned_tx.output[0].value = Amount::from_sat(49_999); + variants.push(changed); + let mut changed = psbt.clone(); + changed.unsigned_tx.output[0].script_pubkey = bitcoin::ScriptBuf::from_bytes(vec![0x52]); + variants.push(changed); + let mut changed = psbt.clone(); + changed.unsigned_tx.lock_time = bitcoin::absolute::LockTime::from_consensus(1); + variants.push(changed); + let mut changed = psbt.clone(); + changed.unsigned_tx.version = bitcoin::transaction::Version::ONE; + variants.push(changed); + let mut changed = psbt.clone(); + changed.unsigned_tx.input[0].sequence = Sequence::from_consensus(1); + variants.push(changed); + let mut changed = psbt.clone(); + changed.unsigned_tx.input[0].previous_output.vout = 2; + variants.push(changed); + for index in 0..2 { + let mut changed = psbt.clone(); + changed.inputs[index].witness_utxo.as_mut().unwrap().value = Amount::from_sat(99_999); + variants.push(changed); + let mut changed = psbt.clone(); + changed.inputs[index] + .witness_utxo + .as_mut() + .unwrap() + .script_pubkey = bitcoin::ScriptBuf::from_bytes(vec![0x52]); + variants.push(changed); + } + for (index, changed) in variants.iter().enumerate() { + assert_ne!(original, hash(changed), "mutation {index}"); + } + for (index, cmr, script, network) in [ + (1, cmr, script.clone(), BitcoinNetwork::Testnet), + (0, cmr, script.clone(), BitcoinNetwork::Mainnet), + (0, cmr, script.clone(), BitcoinNetwork::Testnet4), + ( + 0, + Cmr::from_byte_array([2; 32]), + script.clone(), + BitcoinNetwork::Testnet, + ), + (0, cmr, Script::new(), BitcoinNetwork::Testnet), + ] { + assert_ne!( + original, + bitcoin_execution_environment(&psbt, index, cmr, &script, network) + .unwrap() + .approval_hash + ); + } + assert_eq!( + original, + bitcoin_execution_environment(&psbt, 0, cmr, &script, BitcoinNetwork::Testnet) + .unwrap() + .approval_hash, ); + } - let pset: PartiallySignedTransaction = deserialize(&pset_bytes).expect("valid PSET"); + #[test] + fn bitcoin_context_rejects_missing_or_inconsistent_inputs() { + let mut psbt = approval_psbt(); + let context = |psbt: &psbt::Psbt, index| { + bitcoin_execution_environment( + psbt, + index, + Cmr::from_byte_array([1; 32]), + &Script::new(), + BitcoinNetwork::Testnet, + ) + }; + assert!(matches!( + context(&psbt, 2), + Err(RunnerError::InputIndexOutOfRange { .. }) + )); + psbt.inputs[1].witness_utxo = None; + assert!(matches!( + context(&psbt, 0), + Err(RunnerError::MissingWitnessUtxo(1)) + )); + psbt.inputs.pop(); + assert!(matches!( + context(&psbt, 0), + Err(RunnerError::PsbtInputCountMismatch { .. }) + )); + } - _ = SimplicityRunner::execute_elements( - program, - Some(""), - 0, - &pset, - script, - crate::ElementsNetwork::LiquidTestnet, - ) - .expect("should run") + #[test] + fn approval_jet_returns_the_context_digest_and_plugins_receive_the_transaction() { + use crate::plugin::PluginBase; + use simplicity::{Word, jet::Core}; + let psbt = approval_psbt(); + let env = UnchainedEnv::new( + Script::new(), + bitcoin_execution_environment( + &psbt, + 1, + Cmr::from_byte_array([1; 32]), + &Script::new(), + BitcoinNetwork::Testnet, + ) + .unwrap(), + ); + let redeem = types::Context::with_context(|ctx| { + let actual = Arc::::jet(&ctx, &CoreExtension::TxApprovalHash); + let expected = + Arc::::const_word(&ctx, Word::u256(env.env.approval_hash)); + let pair = Arc::::pair(&actual, &expected).unwrap(); + let eq = Arc::::jet(&ctx, &CoreExtension::Core(Core::Eq256)); + let equal = Arc::::comp(&pair, &eq).unwrap(); + let verify = Arc::::jet(&ctx, &CoreExtension::Core(Core::Verify)); + Arc::::comp(&equal, &verify) + .unwrap() + .finalize_unpruned() + .unwrap() + }); + bounded_machine(&redeem) + .unwrap() + .exec(&redeem, &env) + .unwrap(); + assert_eq!(CoreExtension::input_index(&env), 1); + assert_eq!( + CoreExtension::tx(&env), + simplicity::bitcoin::consensus::serialize(&psbt.unsigned_tx) + ); } } diff --git a/core/src/utils.rs b/core/src/utils.rs index 199fe8b..04f3605 100644 --- a/core/src/utils.rs +++ b/core/src/utils.rs @@ -1,11 +1,15 @@ -use hal_simplicity::simplicity::bitcoin; -use hal_simplicity::simplicity::elements::{ +use std::str::FromStr; +use std::sync::LazyLock; + +use simplicity::bitcoin::KnownHrp; +use simplicity::bitcoin::taproot::TaprootSpendInfo; +use simplicity::elements::{ self, bitcoin::PublicKey, - hashes::Hash as ElementsHash, secp256k1_zkp::{Secp256k1, SecretKey, rand::rngs::OsRng}, }; -use hal_simplicity::simplicity::hashes::{HashEngine, sha256}; +use simplicity::hashes::{HashEngine, sha256}; +use simplicity::{Cmr, ToXOnlyPubkey, bitcoin}; use thiserror::Error; @@ -14,14 +18,51 @@ use thiserror::Error; pub enum UtilsError { #[error("Expected 2 public keys for 2-of-2 multisig, got {0}")] InvalidPublicKeyCount(usize), + #[error(transparent)] + TaprootBuilderErrorBtc(bitcoin::taproot::TaprootBuilderError), + #[error(transparent)] + TaprootBuilderErrorElements(elements::taproot::TaprootBuilderError), + #[error("Failed to finalize taproot")] + TaprootFinalizationError, } const SIMPLICITY_TAG_PREFIX: &[u8] = b"Simplicity\x1fCommitment\x1f"; -const JETIV: sha256::Midstate = sha256::Midstate([ - 0x95, 0x32, 0xee, 0x28, 0xcd, 0xca, 0x69, 0xde, 0xc8, 0xa0, 0xa2, 0x18, 0xb7, 0x9b, 0xe3, 0x62, - 0xf7, 0x40, 0xce, 0xaf, 0x64, 0x7f, 0x15, 0xb3, 0x8a, 0xed, 0x91, 0x68, 0x16, 0x3f, 0x92, 0x1b, -]); +/// Version 1 key commitment: tagged SHA256 of `CMR || SHA256(redeem_script)`. +/// +/// The script bytes must be identical at address generation and execution time. +/// Empty scripts are committed too; this never falls back to a CMR-only tweak. +pub fn signing_commitment(cmr: Cmr, redeem_script: &[u8]) -> [u8; 32] { + let tag = sha256::Hash::hash(b"SimplicityUnchained/SigningCommitment/v1"); + let mut engine = sha256::Hash::engine(); + engine.input(tag.as_byte_array()); + engine.input(tag.as_byte_array()); + engine.input(&cmr.to_byte_array()); + engine.input(sha256::Hash::hash(redeem_script).as_byte_array()); + sha256::Hash::from_engine(engine).to_byte_array() +} + +const JETIV: sha256::Midstate = sha256::Midstate::new( + [ + 0x95, 0x32, 0xee, 0x28, 0xcd, 0xca, 0x69, 0xde, 0xc8, 0xa0, 0xa2, 0x18, 0xb7, 0x9b, 0xe3, + 0x62, 0xf7, 0x40, 0xce, 0xaf, 0x64, 0x7f, 0x15, 0xb3, 0x8a, 0xed, 0x91, 0x68, 0x16, 0x3f, + 0x92, 0x1b, + ], + 0, +); + +/// The standard NUMS internal key for Taproot outputs. +/// +/// Used as the internal key in P2TR outputs that enforce spending exclusively +/// via script-path. +/// +/// Reference: BIP-341 — +pub static UNSPENDABLE_KEY_P2TR: LazyLock = LazyLock::new(|| { + bitcoin::XOnlyPublicKey::from_str( + "50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0", + ) + .unwrap() +}); // Warning: The CMRs generated here does not follow the proper Simplicity specification. // @@ -36,7 +77,7 @@ fn cmr(name: &str) -> [u8; 32] { let right_state = sha256::Hash::hash(&name).as_byte_array().to_owned(); - let mut engine = sha256::HashEngine::from_midstate(JETIV, 0); + let mut engine = sha256::HashEngine::from_midstate(JETIV); engine.input(&right_state); right_state @@ -56,108 +97,405 @@ pub fn generate_keypair() -> (SecretKey, PublicKey) { (secret_key, public_key) } -/// Generate a 2-of-2 multisig address from a list of public keys -/// Returns the address and the redeem script -pub fn generate_2of2_multisig_address_elements( - pubkeys: &[PublicKey], - address_params: &'static elements::AddressParams, -) -> Result<(elements::Address, elements::script::Script), UtilsError> { - if pubkeys.len() != 2 { - return Err(UtilsError::InvalidPublicKeyCount(pubkeys.len())); - } +#[derive(Error, Debug)] +pub enum SigExtractionError { + #[error("Input {0}: failed to parse tapscript leaf")] + ParseFail(usize), + #[error("Input {0}: unexpected number of instructions; expected {1}, got {2}")] + ScriptLenMismatch(usize, usize, usize), + #[error("Input {0}: invalid xonly pk at position {1}")] + InvalidPubKey(usize, usize), + #[error("Input {0}: expected 32-byte xonly pk at position {1}")] + ExpectedPubkey(usize, usize), + #[error("Input {0}: unexpected opcode at position {1}")] + UnexpectedOpcode(usize, usize), + #[error("Input {0}: recovery key does not match multisig user key")] + RecoveryKeyMismatch(usize), +} - // Build the 2-of-2 multisig script - let redeem_script = elements::script::Builder::new() +/// Builds the P2TR multisig leaf script for the normal cooperative spend path. +/// Script structure: +/// ```text +/// OP_CHECKSIG OP_CHECKSIGADD OP_2 OP_EQUAL +/// ``` +/// +/// Witness order during finalization: `