diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..2f55f1cf6 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,2 @@ +[env] +ZKM_IMM_WRAP_VK = "1" diff --git a/.claude/commands/bridge-out.md b/.claude/commands/bridge-out.md index abb60388c..743a35685 100644 --- a/.claude/commands/bridge-out.md +++ b/.claude/commands/bridge-out.md @@ -25,11 +25,11 @@ Initiate Bridge Out via the `bridge-out` binary (payInvoice quote -> swap initia 3. Check if the `bridge-out` binary exists at `./bin/bridge-out`. If not, run the install script to download it: ```bash - .claude/commands/install-bitvm2.sh install + .claude/commands/install-bitvm.sh install ``` To upgrade to the latest version: ```bash - .claude/commands/install-bitvm2.sh upgrade + .claude/commands/install-bitvm.sh upgrade ``` The script auto-detects the platform (x86_64-linux / aarch64-macos), downloads from GitHub Releases, verifies the sha256 checksum, and installs all binaries to `./bin/`. diff --git a/.claude/commands/challenge.md b/.claude/commands/challenge.md index ad4a4bf96..c3bf74b79 100644 --- a/.claude/commands/challenge.md +++ b/.claude/commands/challenge.md @@ -31,7 +31,7 @@ Use the `/run-challenger-node` skill to start one, or see `deployment/README.md` 5. Check if the `challenge` binary exists at `./bin/challenge`. If not, run: ```bash - .claude/commands/install-bitvm2.sh install + .claude/commands/install-bitvm.sh install ``` 6. Run the command using the pre-built binary: diff --git a/.claude/commands/install-bitvm2.sh b/.claude/commands/install-bitvm.sh similarity index 97% rename from .claude/commands/install-bitvm2.sh rename to .claude/commands/install-bitvm.sh index 2628dbd4f..a194ed784 100755 --- a/.claude/commands/install-bitvm2.sh +++ b/.claude/commands/install-bitvm.sh @@ -1,10 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -REPO="GOATNetwork/bitvm2-node" +REPO="GOATNetwork/bitvm-node" API_URL="https://api.github.com/repos/${REPO}/releases" INSTALL_DIR="./bin" -VERSION_FILE=".bitvm2-version" +VERSION_FILE=".bitvm-version" usage() { cat </bitvm2-nodes/challenge_0/.env.challenge_0 .env + cp deployment//bitvm-noded/challenge_0/.env.challenge_0 .env ``` -3. Check if the `bitvm2-noded` binary exists at `./bin/bitvm2-noded`. If not, run: +3. Check if the `bitvm-noded` binary exists at `./bin/bitvm-noded`. If not, run: ```bash - .claude/commands/install-bitvm2.sh install + .claude/commands/install-bitvm.sh install ``` 4. Start the challenger node: ```bash -./bin/bitvm2-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" +./bin/bitvm-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" ``` To run in the background: ```bash -nohup ./bin/bitvm2-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" >challenger_$(date +'%Y%m%d').log 2>&1 & +nohup ./bin/bitvm-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" >challenger_$(date +'%Y%m%d').log 2>&1 & ``` 5. Verify the node is running: @@ -49,9 +49,9 @@ Should return `Hello, World!`. ### Example (testnet4) ```bash -cp deployment/testnet4/bitvm2-nodes/challenge_0/.env.challenge_0 .env +cp deployment/testnet4/bitvm-noded/challenge_0/.env.challenge_0 .env # Edit .env to fill in BITVM_SECRET, GOAT_ADDRESS, PEER_KEY -./bin/bitvm2-noded --rpc-addr 127.0.0.1:8906 --db-path sqlite:$PWD/bitvm2-node.db --p2p-port 8449 --bootnodes /ip4/34.215.238.232/tcp/8445/p2p/12D3KooWCrPTAmhFdC5DBGgkxZvJi6iuSeiDWKRL87isrt4iMHXv +./bin/bitvm-noded --rpc-addr 127.0.0.1:8906 --db-path sqlite:$PWD/bitvm-node.db --p2p-port 8449 --bootnodes /ip4/34.215.238.232/tcp/8445/p2p/12D3KooWCrPTAmhFdC5DBGgkxZvJi6iuSeiDWKRL87isrt4iMHXv ``` For full deployment documentation, see `deployment/README.md` (section **Challenger**). diff --git a/.claude/commands/run-operator-node.md b/.claude/commands/run-operator-node.md index 79323c658..b82f3c840 100644 --- a/.claude/commands/run-operator-node.md +++ b/.claude/commands/run-operator-node.md @@ -1,4 +1,4 @@ -Run a BitVM2 Operator node locally. +Run a BitVM Operator node locally. The Operator manages bridge operations, kickoff processing, and pegout (Gateway.initWithdraw). @@ -8,11 +8,11 @@ The Operator manages bridge operations, kickoff processing, and pegout (Gateway. - **network**: Which network? `testnet4` or `regtest` - **rpc_addr**: RPC listen address. Default: `127.0.0.1:8902` - **p2p_port**: P2P listen port. Default: `8445` (testnet4) or `8446` (regtest) - - **db_path**: SQLite database path. Default: `sqlite:$PWD/bitvm2-node.db` + - **db_path**: SQLite database path. Default: `sqlite:$PWD/bitvm-node.db` 2. Ensure the `.env` file exists in the working directory. Template configs are at: - - **testnet4**: `deployment/testnet4/bitvm2-nodes/operator_0/.env.operator_0` - - **regtest**: `deployment/regtest/bitvm2-nodes/operator_0/.env.operator_0` + - **testnet4**: `deployment/testnet4/bitvm-noded/operator_0/.env.operator_0` + - **regtest**: `deployment/regtest/bitvm-noded/operator_0/.env.operator_0` The user **must** fill in these required secrets: - `BITVM_SECRET` - Operator BTC private key (hex) @@ -24,23 +24,23 @@ The Operator manages bridge operations, kickoff processing, and pegout (Gateway. Copy the template if needed: ```bash - cp deployment//bitvm2-nodes/operator_0/.env.operator_0 .env + cp deployment//bitvm-noded/operator_0/.env.operator_0 .env ``` -3. Check if the `bitvm2-noded` binary exists at `./bin/bitvm2-noded`. If not, run: +3. Check if the `bitvm-noded` binary exists at `./bin/bitvm-noded`. If not, run: ```bash - .claude/commands/install-bitvm2.sh install + .claude/commands/install-bitvm.sh install ``` 4. Start the operator node: ```bash -./bin/bitvm2-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" +./bin/bitvm-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" ``` To run in the background: ```bash -nohup ./bin/bitvm2-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" >operator_$(date +'%Y%m%d').log 2>&1 & +nohup ./bin/bitvm-noded --rpc-addr --db-path --p2p-port --bootnodes "$BOOTNODES" >operator_$(date +'%Y%m%d').log 2>&1 & ``` 5. Verify the node is running: @@ -52,9 +52,9 @@ Should return `Hello, World!`. ### Example (testnet4) ```bash -cp deployment/testnet4/bitvm2-nodes/operator_0/.env.operator_0 .env +cp deployment/testnet4/bitvm-noded/operator_0/.env.operator_0 .env # Edit .env to fill in BITVM_SECRET, GOAT_ADDRESS, PEER_KEY, GOAT_PRIVATE_KEY -./bin/bitvm2-noded --rpc-addr 127.0.0.1:8902 --db-path sqlite:$PWD/bitvm2-node.db --p2p-port 8445 --bootnodes /ip4/34.215.238.232/tcp/8445/p2p/12D3KooWCrPTAmhFdC5DBGgkxZvJi6iuSeiDWKRL87isrt4iMHXv +./bin/bitvm-noded --rpc-addr 127.0.0.1:8902 --db-path sqlite:$PWD/bitvm-node.db --p2p-port 8445 --bootnodes /ip4/34.215.238.232/tcp/8445/p2p/12D3KooWCrPTAmhFdC5DBGgkxZvJi6iuSeiDWKRL87isrt4iMHXv ``` For full deployment documentation, see `deployment/README.md` (section **Operator**). diff --git a/.claude/commands/upgrade.md b/.claude/commands/upgrade.md index 29de3063e..1663841fb 100644 --- a/.claude/commands/upgrade.md +++ b/.claude/commands/upgrade.md @@ -1,19 +1,19 @@ -Upgrade or install bitvm2-node binaries via `install-bitvm2.sh`. +Upgrade or install bitvm-node binaries via `install-bitvm.sh`. ## Instructions 1. Check the currently installed version: ```bash - .claude/commands/install-bitvm2.sh version + .claude/commands/install-bitvm.sh version ``` 2. If $ARGUMENTS contains a target version (e.g. `v0.3.2`), use that version. Otherwise, upgrade to the latest release. 3. Run the upgrade: ```bash - .claude/commands/install-bitvm2.sh upgrade $ARGUMENTS + .claude/commands/install-bitvm.sh upgrade $ARGUMENTS ``` -4. If the script is missing or not executable, inform the user that `.claude/commands/install-bitvm2.sh` is required and offer to check if it exists. +4. If the script is missing or not executable, inform the user that `.claude/commands/install-bitvm.sh` is required and offer to check if it exists. 5. Report the result to the user: what version was installed before, what version is installed now, and list the installed binaries. diff --git a/.env.template b/.env.template index 6594073d6..1c315f44e 100644 --- a/.env.template +++ b/.env.template @@ -1,10 +1,10 @@ RUST_LOG=debug # required -ACTOR=Challenger +ACTOR=Verifier # required -GOAT_GATEWAY_CONTRACT_ADDRESS=0xc3c2D73FB80cbc10DD85B6896e639D47ebd21B01 +GOAT_GATEWAY_CONTRACT_ADDRESS= # required GOAT_CHAIN_URL=https://rpc.testnet3.goat.network @@ -12,7 +12,7 @@ GOAT_CHAIN_URL=https://rpc.testnet3.goat.network # required PEER_KEY= -# required, operator and challenger should setup its BTC private key, committee can setup a seed string. +# required, operator and verifier should setup its BTC private key, committee can setup a seed string. BITVM_SECRET= # optional, for relayer only @@ -22,7 +22,7 @@ GOAT_PRIVATE_KEY= GOAT_ADDRESS= # protocol name -PROTO_NAME=bitvm2t3 +PROTO_NAME=bitvmt3 # setup bootnodes, separate multiple nodes by comma ",". BOOTNODES= \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 554b2c105..65ed43d2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,6 +11,7 @@ on: branches: - main - dev + - gc-v2 env: CARGO_TERM_COLOR: always @@ -20,8 +21,105 @@ concurrency: cancel-in-progress: true jobs: + # Runs first and gates everything else (fmt/clippy/test all `needs: tla-plus` + # below) - it's the fastest job (seconds, no Rust toolchain to build) and a + # failure here means either a real regression the other, much slower jobs + # can't catch, or a stale/broken spec - either way not worth burning 30+ + # minutes of Cargo Test/Clippy compute on before finding out. + # + # All 8 findings from audit/TLAPlus-20260630.md were fixed in commit + # 991faaa, so this job is expected to be GREEN. Its steps still run every + # bug config that reproduced the original counterexamples, but a bug + # config correctly still failing is no longer treated as a job failure - + # see the second step's own comment for why (its constants are frozen + # historical snapshots, not a live read of the Rust source, so they can't + # detect a regression by staying red; only an unexpected PASS is a real + # drift signal now). + tla-plus: + name: TLA+ Formal Verification + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "21" + - name: Download TLA+ tools + run: | + mkdir -p ~/.local/share/tlaplus + curl -sL -o ~/.local/share/tlaplus/tla2tools.jar \ + https://github.com/tlaplus/tlaplus/releases/latest/download/tla2tools.jar + # This audit pass proves bugs exist in CURRENT code and proves correct + # fix designs for them - the fixes are NOT yet applied to the Rust code + # (see node/README.md's "Known gap" sections). These configs model the + # verified fix designs (or a baseline that was never buggy) and must + # always pass. See root README.md's "Formal verification (TLA+)" + # section for what each spec covers. + - name: Run baseline + proposed-fix specs (must pass) + working-directory: node/tla + run: | + set -e + JAR=~/.local/share/tlaplus/tla2tools.jar + java -jar "$JAR" -config GraphLifecycleCoreOnly.cfg GraphLifecycle.tla + java -jar "$JAR" -config GraphLifecycleFixed.cfg GraphLifecycle.tla + java -jar "$JAR" -config GraphLifecycleFineGrainedFixed.cfg GraphLifecycleFineGrainedFixed.tla + java -jar "$JAR" -config InstancePresignedFixed.cfg InstancePresigned.tla + java -jar "$JAR" -config Take2DisproveRace.cfg Take2DisproveRace.tla + java -jar "$JAR" -config MultiActorRace.cfg MultiActorRace.tla + java -jar "$JAR" -config InstanceBridgeOutRaceFixed.cfg InstanceBridgeOutRace.tla + java -jar "$JAR" -config MessageStateRaceFixed.cfg MessageStateRace.tla + java -jar "$JAR" -config Take1ChallengeRaceFixed.cfg Take1ChallengeRace.tla + # This job's earlier design (while all 8 findings from this round were + # still genuinely unfixed) made this step - and everything gated + # behind it - fail for as long as any bug config still reproduced its + # counterexample. As of commit 991faaa, every one of those findings + # has actually been fixed in the shipped Rust code (see + # audit/TLAPlus-20260630.md) - keeping the job permanently red past + # that point stopped being useful: these bug configs' constants are + # frozen historical snapshots (e.g. Take1ChallengeRace.tla's + # ConnectorA), not live readings of the current Rust source, so they + # can never detect a real code regression by themselves - they will + # keep reproducing the same counterexample forever regardless of + # what the Rust code does. Their only genuine ongoing signal is the + # OPPOSITE direction: if one of them ever unexpectedly STOPS + # reproducing its counterexample, that means the spec itself was + # edited into no longer demonstrating the bug it's supposed to - + # that's the one case this step still treats as a hard failure. + # Otherwise, a bug config correctly still failing is expected and + # does not fail the job - it's just printed as an informational + # reproduction pointer. + - name: Confirm known-bug specs still reproduce their counterexample + working-directory: node/tla + run: | + JAR=~/.local/share/tlaplus/tla2tools.jar + { + echo "## TLA+ audit: historical bug-reproduction specs" + echo + echo "These model the PRE-FIX code as a permanent historical record (all" + echo "findings below were fixed in commit 991faaa - see" + echo "\`audit/TLAPlus-20260630.md\`). Still correctly reproducing their" + echo "original counterexample below is expected and does not fail this job." + echo + } >> "$GITHUB_STEP_SUMMARY" + while IFS='|' read -r cfg tla finding; do + [ -z "$cfg" ] && continue + if java -jar "$JAR" -config "$cfg" "$tla" | grep -q "Model checking completed. No error has been found."; then + echo "::error::$tla / $cfg was expected to keep reproducing its historical counterexample but passed instead - the spec itself was likely edited into no longer demonstrating the bug it's supposed to. If the underlying Rust fix was somehow reverted, this is also how you'd find out - either way, investigate before trusting this spec again." + exit 1 + fi + repro="cd node/tla && java -jar ~/.local/share/tlaplus/tla2tools.jar -config $cfg $tla" + echo "- **$finding** - reproduce: \`$repro\`" >> "$GITHUB_STEP_SUMMARY" + done <<'BUGS' + GraphLifecycle.cfg|GraphLifecycle.tla|Finding 1: Graph.status race + GraphLifecycleFineGrained.cfg|GraphLifecycleFineGrained.tla|Finding 1b: naive guard still unsafe + InstancePresignedBug.cfg|InstancePresigned.tla|Finding 2: Instance.status regression past Presigned + InstanceBridgeOutRace.cfg|InstanceBridgeOutRace.tla|Finding 6: InstanceBridgeOutStatus resurrection + MessageStateRace.cfg|MessageStateRace.tla|Finding 7: MessageState resurrection + Take1ChallengeRace.cfg|Take1ChallengeRace.tla|Finding 9: connector_a has no margin check + BUGS fmt: name: Rustfmt + needs: tla-plus runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -38,6 +136,7 @@ jobs: args: --all -- --check clippy: name: Clippy + needs: tla-plus runs-on: ubuntu-latest steps: - uses: actions/checkout@v5 @@ -47,12 +146,16 @@ jobs: profile: minimal toolchain: nightly-2025-12-11 override: true + components: clippy + - run: curl --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/ProjectZKM/toolchain/refs/heads/main/setup.sh | sh - name: Install Dependencies run: sudo apt update && sudo apt install protobuf-compiler - - run: rustup component add clippy - - run: cargo clippy --all-targets -- -D warnings + - run: | + source ~/.zkm-toolchain/env + cargo clippy --all-targets -- -D warnings test: name: Cargo Test + needs: tla-plus runs-on: ubuntu-latest strategy: matrix: @@ -67,7 +170,7 @@ jobs: - name: Launch the Regtest run: cd scripts && docker compose up -d - name: Run all unit tests - run: | + run: | set -e - source ~/.zkm-toolchain/env + source ~/.zkm-toolchain/env cargo test -r --all --all-targets diff --git a/.github/workflows/ci_time_consuming.yml b/.github/workflows/ci_time_consuming.yml index 33a1c496b..666e18728 100644 --- a/.github/workflows/ci_time_consuming.yml +++ b/.github/workflows/ci_time_consuming.yml @@ -18,5 +18,5 @@ jobs: - name: Run all unit tests run: | source ~/.zkm-toolchain/env - cd crates/bitvm2-ga + cd crates/bitvm-gc cargo test -r --features ci-tests -- --nocapture diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dd2358e22..eefb99acf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -74,7 +74,7 @@ jobs: - name: Collect binaries run: | BINS=( - "target/release/bitvm2-noded" + "target/release/bitvm-noded" "target/release/sequencer-set-publish" "target/release/challenge" "target/release/send-rbf" @@ -106,7 +106,7 @@ jobs: fi done - ARCHIVE_NAME="bitvm2-node-${VERSION}-${PLATFORM}.tar.gz" + ARCHIVE_NAME="bitvm-node-${VERSION}-${PLATFORM}.tar.gz" tar -czf "release-artifacts/${ARCHIVE_NAME}" -C "$STAGING_DIR" . $CHECKSUM_CMD "release-artifacts/${ARCHIVE_NAME}" > "release-artifacts/${ARCHIVE_NAME}.sha256" @@ -129,4 +129,4 @@ jobs: echo "### Version: $VERSION" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### Archive:" >> $GITHUB_STEP_SUMMARY - ls -1 release-artifacts/ | sed 's/^/- /' >> $GITHUB_STEP_SUMMARY \ No newline at end of file + ls -1 release-artifacts/ | sed 's/^/- /' >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore index 13c8f4ab7..af69b8b73 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,5 @@ circuits/*/*/*.bin.in **/*.out **/*/output.data* -proof-builder-rpc/*.ckpt \ No newline at end of file +proof-builder-rpc/*.ckpt +node/tla/states/ \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8f8f2862d..df8944573 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -44,7 +44,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "generic-array 0.14.7", ] @@ -116,7 +116,7 @@ dependencies = [ "alloy-rpc-client 1.8.3", "alloy-rpc-types 1.8.3", "alloy-serde 1.8.3", - "alloy-signer 1.8.3 (git+https://github.com/alloy-rs/alloy)", + "alloy-signer 1.8.3", "alloy-signer-local", "alloy-transport 1.8.3", "alloy-transport-http 1.8.3", @@ -501,7 +501,7 @@ dependencies = [ "alloy-rpc-types-any 1.8.3", "alloy-rpc-types-eth 1.8.3", "alloy-serde 1.8.3", - "alloy-signer 1.8.3 (git+https://github.com/alloy-rs/alloy)", + "alloy-signer 1.8.3", "alloy-sol-types", "async-trait", "auto_impl", @@ -651,7 +651,7 @@ dependencies = [ "alloy-rpc-types-eth 1.8.3", "alloy-rpc-types-trace 1.8.3", "alloy-rpc-types-txpool", - "alloy-signer 1.8.3 (git+https://github.com/alloy-rs/alloy)", + "alloy-signer 1.8.3", "alloy-sol-types", "alloy-transport 1.8.3", "alloy-transport-http 1.8.3", @@ -988,21 +988,6 @@ dependencies = [ "thiserror 2.0.18", ] -[[package]] -name = "alloy-signer" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43f447aefab0f1c0649f71edc33f590992d4e122bc35fb9cdbbf67d4421ace85" -dependencies = [ - "alloy-primitives", - "async-trait", - "auto_impl", - "either", - "elliptic-curve", - "k256", - "thiserror 2.0.18", -] - [[package]] name = "alloy-signer" version = "1.8.3" @@ -1025,7 +1010,7 @@ dependencies = [ "alloy-consensus 1.8.3", "alloy-network 1.8.3", "alloy-primitives", - "alloy-signer 1.8.3 (git+https://github.com/alloy-rs/alloy)", + "alloy-signer 1.8.3", "async-trait", "k256", "rand 0.8.5", @@ -1345,6 +1330,15 @@ version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" +[[package]] +name = "arc-swap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a3a1fd6f75306b68087b831f025c712524bcb19aad54e557b1129cfa0a2b207" +dependencies = [ + "rustversion", +] + [[package]] name = "ark-bls12-381" version = "0.5.0" @@ -1995,6 +1989,49 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" +[[package]] +name = "aws-config" +version = "1.8.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33f815b73a3899c03b380d543532e5865f230dce9678d108dc10732a8682275" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "hex", + "http 1.4.0", + "sha1 0.10.6", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f20799b373a1be121fe3005fba0c2090af9411573878f224df44b42727fcaf7" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + [[package]] name = "aws-lc-rs" version = "1.16.2" @@ -2017,6 +2054,414 @@ dependencies = [ "fs_extra", ] +[[package]] +name = "aws-runtime" +version = "1.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ed8e8c52d2dc2390ad9f15647fe663f71e9780b4262c190fbb823a32721566" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "bytes-utils", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid 1.23.0", +] + +[[package]] +name = "aws-sdk-s3" +version = "1.135.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f97e3e7e7d86fd26fcdc18bc382da5ca9e8b2ff8d54030d187fd0dac8a236d96" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-checksums", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "bytes", + "fastrand", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "http-body 1.0.1", + "lru 0.16.3", + "percent-encoding", + "regex-lite", + "sha2 0.11.0", + "tracing", + "url", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b647baea49ff551960b904f905681e9b4765a6c4ea08631e89dc52d8bd3f5896" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.103.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ae401c65ff288aa7873117fe535cd32b7b1bb0bc43751d28901a1d5f20636b9" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.106.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c80de7bb7d03e9ca8c9fd7b489f20f3948d3f3be91a7953591347d238115408" +dependencies = [ + "arc-swap", + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-observability", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bae38512beae0ffee7010fc24e7a8a123c53efdfef42a61e80fda4882418dc71" +dependencies = [ + "aws-credential-types", + "aws-smithy-eventstream", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint", + "form_urlencoded", + "hex", + "hmac 0.13.0", + "http 0.2.12", + "http 1.4.0", + "p256", + "percent-encoding", + "sha2 0.11.0", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffcaf626bdda484571968400c326a244598634dc75fd451325a54ad1a59acfc" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-checksums" +version = "0.64.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9e8e65f4f81fcccdeb6c3eca2af17ac21d421a1786a26a394aecf421d616d3a" +dependencies = [ + "aws-smithy-http", + "aws-smithy-types", + "bytes", + "crc-fast", + "hex", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "md-5 0.11.0", + "pin-project-lite", + "sha1 0.11.0", + "sha2 0.11.0", + "tracing", +] + +[[package]] +name = "aws-smithy-eventstream" +version = "0.60.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf09d74e5e32f76b8762da505a3cd59303e367a664ca67295387baa8c1d7548" +dependencies = [ + "aws-smithy-types", + "bytes", + "crc32fast", +] + +[[package]] +name = "aws-smithy-http" +version = "0.63.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1ab2dc1c2c3749ead27180d333c42f11be8b0e934058fb4b2258ee8dbe5231" +dependencies = [ + "aws-smithy-eventstream", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "futures-util", + "http 1.4.0", + "http-body 1.0.1", + "http-body-util", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3ef8931ad1c98aa6a55b4256f847f3116090819844e0dd41ea682cac5dd2d3" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.13", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.9.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.37", + "rustls-native-certs 0.8.3", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.4", + "tower 0.5.3", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.62.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "701a947f4797e52a911e114a898667c746c39feea467bbd1abd7b3721f702ffa" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06c2315d173edbf1920da8ba3a7189695827002e4c0fc961973ab1c54abca9c" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a56d79744fb3edb5d722ef79d86081e121d3b9422cb209eb03aea6aa4f21ebd" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e6f5caf6fea86f8c2206541ab5857cfcda9013426cdbe8fa0098b9e2d32182" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "bytes", + "fastrand", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9db177daa6ba8afb9ee1aefcf548c907abcf52065e394ee11a92780057fe0e8c" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api-macros", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.4.0", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-runtime-api-macros" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d7396fd9500589e62e460e987ecb671bad374934e55ec3b5f498cc7a8a8a7b7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "aws-smithy-schema" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7442cb268338f0eb8278140a107c046756aa01093d8ef5e99628d34ae09c94f5" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "http 1.4.0", +] + +[[package]] +name = "aws-smithy-types" +version = "1.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f93074121a1be41317b9aa607143ae17900631f7f59a99f2b905d519d6783b" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.4.0", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce02add1aa3677d022f8adf81dcbe3046a95f17a1b1e8979c145cd21d3d22b3" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16bf10b03a3c01e6b3b7d47cd964e873ffe9e7d4e80fad16bd4c077cb068531" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-schema", + "aws-smithy-types", + "rustc_version 0.4.1", + "tracing", +] + [[package]] name = "axum" version = "0.6.20" @@ -2225,8 +2670,8 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" dependencies = [ - "bitcoin-internals 0.3.0", - "bitcoin_hashes 0.14.1", + "bitcoin-internals", + "bitcoin_hashes", ] [[package]] @@ -2247,6 +2692,16 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + [[package]] name = "base64ct" version = "1.8.3" @@ -2259,12 +2714,6 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" -[[package]] -name = "bech32" -version = "0.10.0-beta" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98f7eed2b2781a6f0b5c903471d48e15f56fb4e1165df8a9a2337fd1a59d45ea" - [[package]] name = "bech32" version = "0.11.1" @@ -2360,20 +2809,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "bitcoin" -version = "0.31.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69197dee21fe23b45f5239bf88086efaa0cb8679f3e704906eb818e8ea169c14" -dependencies = [ - "bech32 0.10.0-beta", - "bitcoin-internals 0.2.1", - "bitcoin_hashes 0.13.1", - "hex-conservative 0.1.2", - "hex_lit", - "secp256k1 0.28.2", -] - [[package]] name = "bitcoin" version = "0.32.8" @@ -2382,22 +2817,16 @@ checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" dependencies = [ "base58ck", "bech32 0.11.1", - "bitcoin-internals 0.3.0", + "bitcoin-internals", "bitcoin-io", "bitcoin-units", - "bitcoin_hashes 0.14.1", - "hex-conservative 0.2.2", + "bitcoin_hashes", + "hex-conservative", "hex_lit", "secp256k1 0.29.1", "serde", ] -[[package]] -name = "bitcoin-internals" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "994dc6fcc13751c85370b7de118e672b193b9b65167bf09e258f124c97fb9685" - [[package]] name = "bitcoin-internals" version = "0.3.0" @@ -2415,12 +2844,12 @@ checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-light-client-circuit" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", "bincode", - "bitcoin 0.32.8", + "bitcoin", "blake3", "borsh", "commit-chain", @@ -2432,11 +2861,11 @@ dependencies = [ "revm-database-interface", "serde", "serde_json", - "sha2 0.10.9", "state-chain", "tendermint", "tendermint-light-client-verifier", "tracing", + "verifier", "zkm-primitives", "zkm-verifier", "zkm-zkvm", @@ -2447,7 +2876,7 @@ name = "bitcoin-script" version = "0.4.0" source = "git+https://github.com/BitVM/rust-bitcoin-script#01b4cb66cbf5b525079cabe006f9f99627da97cd" dependencies = [ - "bitcoin 0.32.8", + "bitcoin", "script-macro", "stdext", ] @@ -2457,7 +2886,7 @@ name = "bitcoin-script-stack" version = "0.0.1" source = "git+https://github.com/BitVM/rust-bitcoin-script-stack#643c5f1a44af448274849c01a5ae7fbdd54d8213" dependencies = [ - "bitcoin 0.32.8", + "bitcoin", "bitcoin-script", "bitcoin-scriptexec", ] @@ -2467,7 +2896,7 @@ name = "bitcoin-scriptexec" version = "0.0.0" source = "git+https://github.com/BitVM/rust-bitcoin-scriptexec#ba96bc2bd76774c9d1b011461cb79d983c2c43a1" dependencies = [ - "bitcoin 0.32.8", + "bitcoin", "clap", "console_error_panic_hook", "getrandom 0.2.17", @@ -2484,19 +2913,10 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5285c8bcaa25876d07f37e3d30c303f2609179716e11d688f51e8f1fe70063e2" dependencies = [ - "bitcoin-internals 0.3.0", + "bitcoin-internals", "serde", ] -[[package]] -name = "bitcoin_hashes" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "446819536d8121575eeb7e89efdbadb3f055e87e4bb66c6679a6d5cc2f4b64fd" -dependencies = [ - "hex-conservative 0.1.2", -] - [[package]] name = "bitcoin_hashes" version = "0.14.1" @@ -2504,32 +2924,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" dependencies = [ "bitcoin-io", - "hex-conservative 0.2.2", - "serde", -] - -[[package]] -name = "bitcoincore-rpc" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aedd23ae0fd321affb4bbbc36126c6f49a32818dc6b979395d24da8c9d4e80ee" -dependencies = [ - "bitcoincore-rpc-json", - "jsonrpc", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "bitcoincore-rpc-json" -version = "0.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8909583c5fab98508e80ef73e5592a651c954993dc6b7739963257d19f0e71a" -dependencies = [ - "bitcoin 0.32.8", + "hex-conservative", "serde", - "serde_json", ] [[package]] @@ -2573,7 +2969,7 @@ dependencies = [ "ark-relations", "ark-serialize 0.5.0", "ark-std 0.5.0", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-script", "bitcoin-script-stack", "bitcoin-scriptexec", @@ -2592,42 +2988,80 @@ dependencies = [ ] [[package]] -name = "bitvm2-lib" -version = "0.3.3" +name = "bitvm" +version = "0.1.0" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#bab46c2336ad98513bcb8cbaff21b5506a4dcd9a" +dependencies = [ + "ark-bn254", + "ark-crypto-primitives", + "ark-ec", + "ark-ff 0.5.0", + "ark-groth16", + "ark-relations", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "bitcoin", + "bitcoin-script", + "bitcoin-script-stack", + "bitcoin-scriptexec", + "blake3", + "colored", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rand 0.8.5", + "rand_chacha 0.3.1", + "regex", + "serde", + "sha2 0.10.9", + "tqdm", +] + +[[package]] +name = "bitvm-gc" +version = "0.4.0" dependencies = [ "anyhow", "ark-bn254", + "ark-crypto-primitives", + "ark-ec", + "ark-ff 0.5.0", "ark-groth16", "ark-serialize 0.5.0", "bincode", - "bitcoin 0.31.3", - "bitcoin 0.32.8", - "bitcoin-light-client-circuit", + "bitcoin", "bitcoin-script", - "bitcoincore-rpc", - "bitvm", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", "chacha20poly1305", "clap", - "client", "esplora-client", + "garbled-snark-verifier", "goat", "hex", "hkdf", "musig2", "rand 0.8.5", + "rand_chacha 0.3.1", + "rayon", + "reqwest 0.11.27", + "reqwest 0.12.28", "secp256k1 0.29.1", "serde", + "serde-big-array", "serde_json", "sha2 0.10.9", + "soldering-host", "strum 0.26.3", "tokio", "tracing", "uuid 1.23.0", + "verifiable-circuit-babe", ] [[package]] -name = "bitvm2-noded" -version = "0.3.3" +name = "bitvm-noded" +version = "0.4.0" dependencies = [ "alloy", "anyhow", @@ -2635,43 +3069,47 @@ dependencies = [ "ark-groth16", "ark-serialize 0.5.0", "async-trait", + "aws-config", + "aws-sdk-s3", "axum 0.8.8", "base64 0.21.7", "bincode", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-light-client-circuit", "bitcoin-script", - "bitvm", - "bitvm2-lib", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", + "bitvm-gc", + "blake3", "borsh", "cbft-rpc", "clap", "client", "commit-chain", - "dirs", "dotenv", "esplora-client", "futures", "goat", + "header-chain", "hex", "http 1.4.0", "http-body-util", "indexmap 2.14.0", - "indicatif", "libp2p", "libp2p-metrics", "libp2p-swarm-derive", "musig2", "once_cell", + "p3-bn254-fr", + "p3-field", "prometheus-client", "proof-builder", "rand 0.8.5", "reqwest 0.12.28", "secp256k1 0.29.1", - "semver 1.0.28", "serde", "serde_json", "sha2 0.10.9", + "state-chain", "store", "strum 0.26.3", "stun-client", @@ -2685,8 +3123,11 @@ dependencies = [ "tracing-subscriber 0.3.23", "util", "uuid 1.23.0", + "verifier", "zeroize", + "zkm-recursion-core", "zkm-sdk", + "zkm-stark", "zkm-verifier", ] @@ -2712,9 +3153,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -2742,6 +3183,15 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "block-buffer" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be" +dependencies = [ + "hybrid-array", +] + [[package]] name = "block2" version = "0.6.2" @@ -2780,7 +3230,7 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#bcf9397426410acc171ad625416b508dc913be8e" +source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#97b215f80e9bfab9ad6641e41b005b6d8e58f748" dependencies = [ "cfg-if", "ff 0.13.1", @@ -2884,6 +3334,16 @@ dependencies = [ "serde", ] +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + [[package]] name = "bzip2" version = "0.4.4" @@ -2953,7 +3413,7 @@ dependencies = [ [[package]] name = "cbft-rpc" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "async-trait", @@ -3065,7 +3525,7 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "inout", "zeroize", ] @@ -3123,12 +3583,12 @@ checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9" [[package]] name = "client" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy", "anyhow", "async-trait", - "bitcoin 0.32.8", + "bitcoin", "esplora-client", "hex", "rand 0.8.5", @@ -3151,6 +3611,12 @@ dependencies = [ "cc", ] +[[package]] +name = "cmov" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a" + [[package]] name = "coins-bip32" version = "0.8.7" @@ -3160,7 +3626,7 @@ dependencies = [ "bs58", "coins-core", "digest 0.10.7", - "hmac", + "hmac 0.12.1", "k256", "serde", "sha2 0.10.9", @@ -3175,7 +3641,7 @@ checksum = "3db8fba409ce3dc04f7d804074039eb68b960b0829161f8e06c95fea3f122528" dependencies = [ "bitvec", "coins-bip32", - "hmac", + "hmac 0.12.1", "once_cell", "pbkdf2 0.12.2", "rand 0.8.5", @@ -3195,7 +3661,7 @@ dependencies = [ "digest 0.10.7", "generic-array 0.14.7", "hex", - "ripemd", + "ripemd 0.1.3", "serde", "serde_derive", "sha2 0.10.9", @@ -3231,12 +3697,12 @@ dependencies = [ [[package]] name = "commit-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", "bincode", - "bitcoin 0.32.8", + "bitcoin", "blake3", "borsh", "guest-executor", @@ -3250,17 +3716,17 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] [[package]] name = "commit-chain-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "bincode", - "bitcoin 0.32.8", + "bitcoin", "clap", "client", "commit-chain", @@ -3275,6 +3741,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber 0.3.23", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -3328,7 +3795,13 @@ dependencies = [ name = "const-oid" version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-oid" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c" [[package]] name = "const-str" @@ -3457,6 +3930,16 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crc-fast" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75b2483e97a5a7da73ac68a05b629f9c53cff58d8ed1c77866079e18b00dba5" +dependencies = [ + "digest 0.10.7", + "spin 0.10.0", +] + [[package]] name = "crc32fast" version = "1.5.0" @@ -3569,6 +4052,15 @@ dependencies = [ "typenum", ] +[[package]] +name = "crypto-common" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453" +dependencies = [ + "hybrid-array", +] + [[package]] name = "csv" version = "1.4.0" @@ -3610,6 +4102,15 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "ctutils" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e" +dependencies = [ + "cmov", +] + [[package]] name = "curve25519-dalek" version = "4.1.3" @@ -3845,7 +4346,7 @@ version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" dependencies = [ - "const-oid", + "const-oid 0.9.6", "pem-rfc7468", "zeroize", ] @@ -3966,11 +4467,23 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", - "const-oid", - "crypto-common", + "const-oid 0.9.6", + "crypto-common 0.1.7", "subtle", ] +[[package]] +name = "digest" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2" +dependencies = [ + "block-buffer 0.12.0", + "const-oid 0.10.2", + "crypto-common 0.2.2", + "ctutils", +] + [[package]] name = "dirs" version = "5.0.1" @@ -4312,8 +4825,8 @@ name = "esplora-client" version = "0.11.0" source = "git+https://github.com/BitVM/rust-esplora-client#a29ee89e6fa003655e179615405761b27e67b973" dependencies = [ - "bitcoin 0.32.8", - "hex-conservative 0.2.2", + "bitcoin", + "hex-conservative", "log", "minreq", "reqwest 0.11.27", @@ -4343,7 +4856,7 @@ dependencies = [ "ctr", "digest 0.10.7", "hex", - "hmac", + "hmac 0.12.1", "pbkdf2 0.11.0", "rand 0.8.5", "scrypt", @@ -4632,7 +5145,7 @@ dependencies = [ "ethers-core", "glob", "home", - "md-5", + "md-5 0.10.6", "num_cpus", "once_cell", "path-slash", @@ -5053,6 +5566,33 @@ dependencies = [ "byteorder", ] +[[package]] +name = "garbled-snark-verifier" +version = "0.1.0" +source = "git+https://github.com/GOATNetwork/bitvm-gc?branch=feat%2Fgoat-bitvm3#64e6373162deda32b88289a87e05093124d36b04" +dependencies = [ + "aes", + "ark-bn254", + "ark-crypto-primitives", + "ark-ec", + "ark-ff 0.5.0", + "ark-relations", + "ark-serialize 0.5.0", + "bincode", + "blake3", + "getrandom 0.2.17", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "once_cell", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rayon", + "serde", + "serde_json", + "serial_test", +] + [[package]] name = "gcd" version = "2.3.0" @@ -5190,7 +5730,7 @@ dependencies = [ [[package]] name = "goat" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=GA#26b0bd61b61b24b50b2d2443a7fda8e58412edfa" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#bab46c2336ad98513bcb8cbaff21b5506a4dcd9a" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -5202,10 +5742,11 @@ dependencies = [ "ark-std 0.5.0", "bincode", "bitcode", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-script", "bitcoin-scriptexec", - "bitvm", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", + "blake3", "clap", "colored", "hex", @@ -5217,6 +5758,7 @@ dependencies = [ "reqwest 0.11.27", "secp256k1 0.29.1", "serde", + "serde-big-array", "serde_json", "serial_test", "sha2 0.10.9", @@ -5253,7 +5795,7 @@ dependencies = [ [[package]] name = "guest-executor" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-consensus 1.0.41", "alloy-evm", @@ -5471,28 +6013,30 @@ dependencies = [ [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ - "bitcoin 0.32.8", + "bincode", + "bitcoin", "borsh", "crypto-bigint", "hex-literal", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] [[package]] name = "header-chain-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "ark-bn254", "ark-groth16", "ark-serialize 0.5.0", "bincode", - "bitcoin 0.32.8", + "bitcoin", "borsh", "clap", "client", @@ -5508,6 +6052,7 @@ dependencies = [ "tracing", "tracing-subscriber 0.3.23", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -5541,12 +6086,6 @@ dependencies = [ "serde", ] -[[package]] -name = "hex-conservative" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "212ab92002354b4819390025006c897e8140934349e8635c9b077f47b4dcbd20" - [[package]] name = "hex-conservative" version = "0.2.2" @@ -5672,7 +6211,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" dependencies = [ - "hmac", + "hmac 0.12.1", ] [[package]] @@ -5684,6 +6223,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "hmac" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "home" version = "0.5.12" @@ -5696,7 +6244,7 @@ dependencies = [ [[package]] name = "host-executor" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-chains", "alloy-consensus 1.0.41", @@ -5812,6 +6360,15 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "hybrid-array" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +dependencies = [ + "typenum", +] + [[package]] name = "hyper" version = "0.14.32" @@ -5867,6 +6424,7 @@ dependencies = [ "futures-util", "http 0.2.12", "hyper 0.14.32", + "log", "rustls 0.21.12", "tokio", "tokio-rustls 0.24.1", @@ -5882,6 +6440,7 @@ dependencies = [ "hyper 1.9.0", "hyper-util", "rustls 0.23.37", + "rustls-native-certs 0.8.3", "rustls-pki-types", "tokio", "tokio-rustls 0.26.4", @@ -6429,18 +6988,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "jsonrpc" -version = "0.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3662a38d341d77efecb73caf01420cfa5aa63c0253fd7bc05289ef9f6616e1bf" -dependencies = [ - "base64 0.13.1", - "minreq", - "serde", - "serde_json", -] - [[package]] name = "jsonwebtoken" version = "8.3.0" @@ -7262,6 +7809,16 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "md-5" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98" +dependencies = [ + "cfg-if", + "digest 0.11.3", +] + [[package]] name = "memchr" version = "2.8.0" @@ -7393,7 +7950,7 @@ dependencies = [ [[package]] name = "mpt" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -7479,7 +8036,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baebdafa62ee63bfbb09d9e9664e9f8ba3f5765efcc813c97a92aebdaa06b8a5" dependencies = [ "base16ct", - "hmac", + "hmac 0.12.1", "once_cell", "rand 0.8.5", "secp", @@ -8041,7 +8598,7 @@ dependencies = [ [[package]] name = "operator-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "alloy-provider 1.0.41", @@ -8050,10 +8607,10 @@ dependencies = [ "ark-groth16", "ark-serialize 0.5.0", "bincode", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-light-client-circuit", "bitcoin-script", - "bitvm2-lib", + "bitvm-gc", "borsh", "cbft-rpc", "chrono", @@ -8080,6 +8637,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "url", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -8092,6 +8650,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + [[package]] name = "p256" version = "0.13.2" @@ -8554,7 +9118,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest 0.10.7", - "hmac", + "hmac 0.12.1", "password-hash", "sha2 0.10.9", ] @@ -8566,7 +9130,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac", + "hmac 0.12.1", ] [[package]] @@ -9050,7 +9614,7 @@ dependencies = [ [[package]] name = "primitives" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-eips 1.0.41", "alloy-genesis 1.0.41", @@ -9155,31 +9719,34 @@ dependencies = [ [[package]] name = "proof-builder" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", - "bitcoin 0.32.8", - "bitcoin-light-client-circuit", + "bitcoin", "commit-chain", "header-chain", + "hex", + "rand 0.8.5", + "secp256k1 0.29.1", "serde", + "serde_json", "sha2 0.10.9", "state-chain", "strum 0.26.3", "thiserror 1.0.69", - "zkm-prover", + "verifier", "zkm-sdk", - "zkm-verifier", ] [[package]] name = "proof-builder-rpc" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "anyhow", + "async-trait", "axum 0.8.8", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-light-client-circuit", "clap", "client", @@ -9192,6 +9759,7 @@ dependencies = [ "operator-proof", "prometheus-client", "proof-builder", + "secp256k1 0.29.1", "serde", "serde_json", "state-chain-proof", @@ -9206,6 +9774,7 @@ dependencies = [ "util", "uuid 1.23.0", "watchtower-proof", + "zkm-sdk", ] [[package]] @@ -9336,7 +9905,7 @@ dependencies = [ [[package]] name = "provider" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-json-rpc 1.0.41", "alloy-provider 1.0.41", @@ -9674,6 +10243,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.10" @@ -10448,7 +11023,7 @@ dependencies = [ "k256", "p256", "revm-primitives", - "ripemd", + "ripemd 0.1.3", "sha2 0.10.9", "substrate-bn 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -10481,7 +11056,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" dependencies = [ - "hmac", + "hmac 0.12.1", "subtle", ] @@ -10523,6 +11098,15 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "ripemd" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dd4211456b4172d7e44261920c25acf07367c4f04bb5f5d54fc21b090d9b159" +dependencies = [ + "digest 0.11.3", +] + [[package]] name = "rlp" version = "0.5.2" @@ -10548,7 +11132,7 @@ dependencies = [ [[package]] name = "rpc-db" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-consensus 1.0.41", "alloy-primitives", @@ -10576,7 +11160,7 @@ version = "0.9.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d" dependencies = [ - "const-oid", + "const-oid 0.9.6", "digest 0.10.7", "num-bigint-dig", "num-integer", @@ -10980,7 +11564,7 @@ name = "script-macro" version = "0.4.0" source = "git+https://github.com/BitVM/rust-bitcoin-script#01b4cb66cbf5b525079cabe006f9f99627da97cd" dependencies = [ - "bitcoin 0.32.8", + "bitcoin", "proc-macro-error", "proc-macro2", "quote", @@ -10992,7 +11576,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" dependencies = [ - "hmac", + "hmac 0.12.1", "pbkdf2 0.11.0", "salsa20", "sha2 0.10.9", @@ -11044,25 +11628,15 @@ dependencies = [ "subtle", ] -[[package]] -name = "secp256k1" -version = "0.28.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" -dependencies = [ - "bitcoin_hashes 0.13.1", - "secp256k1-sys 0.9.2", -] - [[package]] name = "secp256k1" version = "0.29.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9465315bc9d4566e1724f0fffcbcc446268cb522e60f9a27bcded6b19c108113" dependencies = [ - "bitcoin_hashes 0.14.1", + "bitcoin_hashes", "rand 0.8.5", - "secp256k1-sys 0.10.1", + "secp256k1-sys", "serde", ] @@ -11072,21 +11646,12 @@ version = "0.30.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ - "bitcoin_hashes 0.14.1", + "bitcoin_hashes", "rand 0.8.5", - "secp256k1-sys 0.10.1", + "secp256k1-sys", "serde", ] -[[package]] -name = "secp256k1-sys" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5d1746aae42c19d583c3c1a8c646bfad910498e2051c551a7f2e3c0c9fbb7eb" -dependencies = [ - "cc", -] - [[package]] name = "secp256k1-sys" version = "0.10.1" @@ -11182,6 +11747,15 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde-big-array" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11fc7cc2c76d73e0f27ee52abbd64eec84d46f370c88371120433196934e4b7f" +dependencies = [ + "serde", +] + [[package]] name = "serde-wasm-bindgen" version = "0.6.5" @@ -11375,6 +11949,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha1" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha2" version = "0.9.9" @@ -11399,6 +11984,17 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "sha2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" +dependencies = [ + "cfg-if", + "cpufeatures 0.3.0", + "digest 0.11.3", +] + [[package]] name = "sha3" version = "0.10.8" @@ -11579,7 +12175,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -11596,6 +12192,26 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "soldering-host" +version = "1.1.0" +source = "git+https://github.com/GOATNetwork/bitvm-gc?branch=feat%2Fgoat-bitvm3#64e6373162deda32b88289a87e05093124d36b04" +dependencies = [ + "ark-bn254", + "ark-crypto-primitives", + "ark-ff 0.5.0", + "ark-groth16", + "ark-relations", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=GA)", + "garbled-snark-verifier", + "rand 0.8.5", + "rand_chacha 0.3.1", + "tracing", + "verifiable-circuit-babe", + "zkm-build", + "zkm-sdk", +] + [[package]] name = "spin" version = "0.5.2" @@ -11611,6 +12227,12 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spin" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe4ccb98d9c292d56fec89a5e07da7fc4cf0dc11e156b41793132775d3e591" + [[package]] name = "spki" version = "0.7.3" @@ -11729,17 +12351,17 @@ dependencies = [ "generic-array 0.14.7", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "percent-encoding", "rand 0.8.5", "rsa", "serde", - "sha1", + "sha1 0.10.6", "sha2 0.10.9", "smallvec", "sqlx-core", @@ -11768,11 +12390,11 @@ dependencies = [ "futures-util", "hex", "hkdf", - "hmac", + "hmac 0.12.1", "home", "itoa", "log", - "md-5", + "md-5 0.10.6", "memchr", "once_cell", "rand 0.8.5", @@ -11821,13 +12443,13 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus 1.0.41", "alloy-primitives", "base64 0.21.7", "bincode", - "bitcoin 0.32.8", + "bitcoin", "blake3", "borsh", "cosmos-sdk-proto", @@ -11845,13 +12467,14 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] [[package]] name = "state-chain-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus 1.0.41", "alloy-primitives", @@ -11883,6 +12506,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "url", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -11903,10 +12527,10 @@ checksum = "4af28eeb7c18ac2dbdb255d40bee63f203120e1db6b0024b177746ebec7049c1" [[package]] name = "store" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", - "bitcoin 0.32.8", + "bitcoin", "futures", "hex", "indexmap 2.14.0", @@ -12031,7 +12655,7 @@ dependencies = [ [[package]] name = "substrate-bn" version = "0.6.0" -source = "git+https://github.com/ziren-patches/bn.git?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" +source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#0dfeeb1b7bfc21941b6e4964d678abd094a54a6f" dependencies = [ "bytemuck", "byteorder", @@ -12242,7 +12866,7 @@ dependencies = [ "num-traits", "once_cell", "prost 0.13.5", - "ripemd", + "ripemd 0.1.3", "serde", "serde_bytes", "serde_json", @@ -12898,6 +13522,16 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + [[package]] name = "tracing-subscriber" version = "0.2.25" @@ -12917,12 +13551,15 @@ dependencies = [ "nu-ansi-term", "once_cell", "regex-automata", + "serde", + "serde_json", "sharded-slab", "smallvec", "thread_local", "tracing", "tracing-core", "tracing-log", + "tracing-serde", ] [[package]] @@ -12955,7 +13592,7 @@ dependencies = [ "log", "rand 0.8.5", "rustls 0.21.12", - "sha1", + "sha1 0.10.6", "thiserror 1.0.69", "url", "utf-8", @@ -12975,7 +13612,7 @@ dependencies = [ "rand 0.9.3", "rustls 0.23.37", "rustls-pki-types", - "sha1", + "sha1 0.10.6", "thiserror 2.0.18", "utf-8", ] @@ -13110,7 +13747,7 @@ version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" dependencies = [ - "crypto-common", + "crypto-common 0.1.7", "subtle", ] @@ -13161,6 +13798,12 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf-8" version = "0.7.6" @@ -13181,9 +13824,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" -version = "0.3.3" +version = "0.4.0" dependencies = [ - "bitcoin 0.32.8", + "bitcoin", "hex", ] @@ -13250,12 +13893,58 @@ dependencies = [ "time", ] +[[package]] +name = "verifiable-circuit-babe" +version = "0.0.1" +source = "git+https://github.com/GOATNetwork/bitvm-gc?branch=feat%2Fgoat-bitvm3#64e6373162deda32b88289a87e05093124d36b04" +dependencies = [ + "aes", + "ark-bn254", + "ark-crypto-primitives", + "ark-ec", + "ark-ff 0.5.0", + "ark-groth16", + "ark-relations", + "ark-serialize 0.5.0", + "bincode", + "bitcoin", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=GA)", + "blake3", + "cfg-if", + "garbled-snark-verifier", + "p3-maybe-rayon", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rayon", + "ripemd 0.2.0", + "serde", + "sha2 0.10.9", + "tracing", + "zkm-sdk", + "zkm-verifier", + "zkm-zkvm", +] + +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "sha2 0.10.9", + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + [[package]] name = "wait-timeout" version = "0.2.1" @@ -13432,7 +14121,7 @@ dependencies = [ [[package]] name = "watchtower-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "anyhow", @@ -13440,7 +14129,7 @@ dependencies = [ "ark-groth16", "ark-serialize 0.5.0", "bincode", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-light-client-circuit", "borsh", "chrono", @@ -13460,6 +14149,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber 0.3.23", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -14210,6 +14900,12 @@ version = "0.8.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f" +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + [[package]] name = "xmltree" version = "0.10.3" @@ -14395,9 +15091,9 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "hmac", + "hmac 0.12.1", "pbkdf2 0.11.0", - "sha1", + "sha1 0.10.6", "time", "zstd 0.11.2+zstd.1.5.2", ] @@ -14431,8 +15127,8 @@ dependencies = [ [[package]] name = "zkm-build" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "cargo_metadata", @@ -14442,8 +15138,8 @@ dependencies = [ [[package]] name = "zkm-core-executor" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -14483,8 +15179,8 @@ dependencies = [ [[package]] name = "zkm-core-machine" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -14535,8 +15231,8 @@ dependencies = [ [[package]] name = "zkm-cuda" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "ctrlc", @@ -14553,8 +15249,8 @@ dependencies = [ [[package]] name = "zkm-curves" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "cfg-if", "curve25519-dalek", @@ -14577,8 +15273,8 @@ dependencies = [ [[package]] name = "zkm-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "proc-macro2", "quote", @@ -14587,8 +15283,8 @@ dependencies = [ [[package]] name = "zkm-lib" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -14600,10 +15296,11 @@ dependencies = [ [[package]] name = "zkm-primitives" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -14614,12 +15311,13 @@ dependencies = [ "p3-symmetric", "serde", "sha2 0.10.9", + "tracing", ] [[package]] name = "zkm-prover" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -14655,8 +15353,8 @@ dependencies = [ [[package]] name = "zkm-recursion-circuit" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "hashbrown 0.14.5", "itertools 0.13.0", @@ -14688,8 +15386,8 @@ dependencies = [ [[package]] name = "zkm-recursion-compiler" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "backtrace", "itertools 0.13.0", @@ -14709,8 +15407,8 @@ dependencies = [ [[package]] name = "zkm-recursion-core" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "backtrace", "ff 0.13.1", @@ -14746,8 +15444,8 @@ dependencies = [ [[package]] name = "zkm-recursion-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "quote", "syn 1.0.109", @@ -14755,8 +15453,8 @@ dependencies = [ [[package]] name = "zkm-recursion-gnark-ffi" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -14779,11 +15477,10 @@ dependencies = [ [[package]] name = "zkm-sdk" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "alloy-primitives", - "alloy-signer 1.8.3 (registry+https://github.com/rust-lang/crates.io-index)", "anyhow", "async-trait", "bincode", @@ -14826,8 +15523,8 @@ dependencies = [ [[package]] name = "zkm-stark" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -14868,8 +15565,8 @@ dependencies = [ [[package]] name = "zkm-verifier" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "ark-bn254", @@ -14892,7 +15589,7 @@ dependencies = [ "serde", "sha2 0.10.9", "strum_macros 0.26.4", - "substrate-bn 0.6.0 (git+https://github.com/ziren-patches/bn.git?branch=patch-0.6.0)", + "substrate-bn 0.6.0 (git+https://github.com/ziren-patches/bn?branch=patch-0.6.0)", "thiserror 2.0.18", "zkm-core-executor", "zkm-core-machine", @@ -14904,8 +15601,8 @@ dependencies = [ [[package]] name = "zkm-zkvm" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index 88256e8c8..dcecddcfd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace.package] -version = "0.3.3" +version = "0.4.0" edition = "2024" [workspace] @@ -8,15 +8,15 @@ members = [ "node", "proof-builder-rpc", "crates/cbft-rpc", - "crates/bitvm2-ga", + "crates/bitvm-gc", "crates/store", "crates/client", "crates/util", "crates/header-chain", "crates/commit-chain", "crates/state-chain", - #"crates/mara-slipstream-client", "crates/bitcoin-light-client-circuit", + "crates/verifier", "circuits/header-chain-proof/host", "circuits/commit-chain-proof/host", "circuits/state-chain-proof/host", @@ -27,8 +27,8 @@ members = [ default-members = ["node"] [workspace.dependencies] -bitvm = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "GA" } -goat = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "GA" } +bitvm = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "gc-v2" } +goat = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "gc-v2" } libp2p = { version = "0.55.0", features = ["tokio", "dns", "kad", "noise", "tcp", "yamux", "rsa", "ping", "mdns"] } libp2p-swarm-derive = "0.35.0" @@ -45,7 +45,10 @@ borsh = { version = "1.5.4", features = ["derive"] } hex = "0.4.3" indexmap = { version = "2.11.0", features = ["serde"] } ark-bn254 = { version = "0.5.0", features = ["curve", "scalar_field"], default-features = false } +ark-ec = "0.5.0" ark-groth16 = "0.5.0" +ark-ff = "0.5.0" +ark-crypto-primitives = "0.5.0" base64 = "0.21" ark-serialize = "0.5.0" sha2 = "0.10.9" @@ -55,8 +58,10 @@ chacha20poly1305 = "0.10.1" tokio-util = "0.7.15" esplora-client = { git = "https://github.com/BitVM/rust-esplora-client" } serde_json = "1.0.116" +serde-big-array = "0.5.1" toml = "0.8" rand = "0.8.5" +rand_chacha = { version = "0.3", default-features = false } dotenv = "0.15.0" blake3 = "=1.5.1" musig2 = { version = "0.1.0", features = ["serde", "rand"] } @@ -82,13 +87,17 @@ uuid = { version = "1.7", features = ["v4", "serde"] } zeroize = "1.8.1" bincode = "1.3.3" -zkm-build = { git = "https://github.com/ProjectZKM/Ziren" } -zkm-core-executor = { git = "https://github.com/ProjectZKM/Ziren" } -zkm-prover = { git = "https://github.com/ProjectZKM/Ziren" } -zkm-sdk = { git = "https://github.com/ProjectZKM/Ziren" } -zkm-verifier = { git = "https://github.com/ProjectZKM/Ziren" } -zkm-primitives = { git = "https://github.com/ProjectZKM/Ziren" } -zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } +zkm-build = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +zkm-core-executor = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +zkm-prover = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +zkm-sdk = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +zkm-verifier = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +zkm-primitives = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7", features = ["verify"] } +zkm-recursion-core = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +zkm-stark = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7" } +p3-bn254-fr = { git = "https://github.com/ProjectZKM/Plonky3" } +p3-field = { git = "https://github.com/ProjectZKM/Plonky3" } #zkm-build = { path = "../Ziren/crates/build" } #zkm-core-executor = { path = "../Ziren/crates/core/executor" } @@ -96,7 +105,14 @@ zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] #zkm-sdk = { path = "../Ziren/crates/sdk" } #zkm-verifier = { path = "../Ziren/crates/verifier" } -bitvm2-lib = { path = "crates/bitvm2-ga" } +bitvm-lib = { package = "bitvm-gc", path = "crates/bitvm-gc" } +verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm-gc", branch = "feat/goat-bitvm3", features = ["aes"] } +garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm-gc", branch = "feat/goat-bitvm3", features = ["_aes"] } +soldering-host = { git = "https://github.com/GOATNetwork/bitvm-gc", branch = "feat/goat-bitvm3", features = ["aes"] } + +#verifiable-circuit-babe = { path = "../bitvm-gc/verifiable-circuit-babe"} +#garbled-snark-verifier = { path = "../bitvm-gc/garbled-snark-verifier"} +#soldering-host = { path = "../bitvm-gc/babe-programs/soldering/host" } store = { path = "crates/store" } util = { path = "crates/util" } client = { path = "crates/client" } @@ -107,6 +123,7 @@ commit-chain = { path = "crates/commit-chain" } commit-chain-proof = { path = "circuits/commit-chain-proof/host" } cbft-rpc = { path = "crates/cbft-rpc" } state-chain = { path = "crates/state-chain" } +verifier = { path = "crates/verifier" } state-chain-proof = { path = "circuits/state-chain-proof/host" } operator-proof = { path = "circuits/operator-proof/host" } watchtower-proof = { path = "circuits/watchtower-proof/host" } @@ -176,4 +193,4 @@ rust.missing_debug_implementations = "warn" rust.unreachable_pub = "warn" rust.unused_must_use = "deny" rust.rust_2018_idioms = { level = "deny", priority = -1 } -rustdoc.all = "warn" \ No newline at end of file +rustdoc.all = "warn" diff --git a/README.md b/README.md index d2e442b64..6ddf509ff 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# GOAT Bitvm2 Node +# GOAT BitVM Node -GOAT Network's BitVM2 bridge implementation. See [GOAT BitVM2 Whitepaper](https://www.goat.network/bitvm2-whitepaper) for more details. +GOAT Network's BitVM bridge implementation. See [GOAT BitVM Whitepaper](https://www.goat.network/bitvm2-whitepaper) for more details. ## Layout @@ -11,6 +11,76 @@ GOAT Network's BitVM2 bridge implementation. See [GOAT BitVM2 Whitepaper](https: - `deployment`: Deployment scripts and documentation +## Formal verification (TLA+) + +`node/tla/` contains TLA+ specs that formally verify the graph/instance status +state machines and the peg-out timelock configuration against real races and +boundary conditions found in the Rust implementation. This started as an +**audit pass**: the specs proved several real bugs existed and proved a +correct fix design for each. **As of commit +[`991faaa`](https://github.com/GOATNetwork/bitvm-node/commit/991faaabdb56c747103e8f1c6d6477c638ccfc4c), +all 8 of those findings have been fixed and verified in the shipped Rust +code** - see `audit/TLAPlus-20260630.md` for the full report, including what +each real applied fix looks like. + +**CI's `tla-plus` job is expected to be GREEN.** Each bug config (e.g. +`GraphLifecycle.cfg`) is kept as a **permanent historical record**, +deliberately still modeling the pre-fix code, and correctly reproducing its +original counterexample - but that expected failure is only printed as an +informational reproduction pointer in the job summary, it does not fail the +job. The only thing that *does* fail the job is a bug config **unexpectedly +passing**, since that would mean either the spec silently stopped +demonstrating the bug it's supposed to, or (more alarmingly) the fix's guard +got removed again. See that job's own comments in +`.github/workflows/ci.yml` for the full reasoning. + +Concretely: for each bug found, there is a **pair** of configs - one modeling +the pre-fix code (still models it as buggy on purpose - **expected to +fail**, a permanent historical record, not a live issue, and does not fail +CI) and one modeling the fix design (**expected to pass**, and does fail CI +if it doesn't - for every finding below, that design has since actually been +applied to the shipped Rust code, not just proven sound in the abstract). + +**Setup** (once): install a JRE (11+) and download the official TLA+ tools jar: + +```bash +sudo apt-get install -y openjdk-21-jre-headless # or any JRE 11+ +mkdir -p ~/.local/share/tlaplus +curl -sL -o ~/.local/share/tlaplus/tla2tools.jar \ + https://github.com/tlaplus/tlaplus/releases/latest/download/tla2tools.jar +``` + +**Run a spec**: + +```bash +cd node/tla +java -jar ~/.local/share/tlaplus/tla2tools.jar -config .cfg .tla +``` + +| Spec | Config | Models | Result | +|---|---|---|---| +| `GraphLifecycle.tla` | `GraphLifecycleCoreOnly.cfg` | code baseline | pass - chain-scan state machine alone is sound | +| `GraphLifecycle.tla` | `GraphLifecycle.cfg` | **pre-fix code (historical)** | **fails, by design**: unguarded race between the Bitcoin-chain-scan and GoatChain-event writers of `Graph.status` - fixed in `991faaa`, kept failing as a permanent regression check | +| `GraphLifecycle.tla` | `GraphLifecycleFixed.cfg` | fix design (**applied in `991faaa`**) | pass - atomic guard design closes the race | +| `GraphLifecycleFineGrained.tla` | `GraphLifecycleFineGrained.cfg` | pre-fix code (historical) | **fails, by design**: the read/write gap a naive (non-atomic) guard would still have - fixed in `991faaa` | +| `GraphLifecycleFineGrainedFixed.tla` | `GraphLifecycleFineGrainedFixed.cfg` | fix design (**applied in `991faaa`**) | pass - single-statement atomic CAS design closes the gap | +| `InstancePresigned.tla` | `InstancePresignedBug.cfg` | **pre-fix code (historical)** | **fails, by design**: `Instance.status` can regress past `Presigned` - fixed in `991faaa` | +| `InstancePresigned.tla` | `InstancePresignedFixed.cfg` | fix design (**applied in `991faaa`**) | pass - guard design closes the regression | +| `Take2DisproveRace.tla` | `Take2DisproveRace.cfg` | fix design (**applied in `991faaa`**, values updated to match) | pass - Take2 vs. Disprove UTXO race has strict margin on all networks with the real shipped `crates/bitvm-gc/src/timelocks.rs` values (Testnet4 `connector_d` now 40, shipped with more margin than originally proposed); the pre-fix shipped value (34) did **not** have this margin - that boundary case is how this spec found the bug in the first place | +| `MultiActorRace.tla` | `MultiActorRace.cfg` | fix design (**applied in `991faaa`**, values updated to match) | pass - the 1-of-N watchtower/verifier security property holds under the real shipped timelock values, checked against 2 independent actors per role rather than 1 | +| `InstanceBridgeOutRace.tla` | `InstanceBridgeOutRace.cfg` | **pre-fix code (historical)** | **fails, by design**: `InstanceBridgeOutStatus` can be resurrected to `Initialize` after reaching `Claim`/`Timeout`/`Refund` by a stale RPC upsert or maintenance-task write - fixed in `991faaa` | +| `InstanceBridgeOutRace.tla` | `InstanceBridgeOutRaceFixed.cfg` | fix design (**applied in `991faaa`**) | pass - atomic guard design (write only if not already terminal) closes the resurrection | +| `MessageStateRace.tla` | `MessageStateRace.cfg` | **pre-fix code (historical)** | **fails, by design**: `MessageState::Cancelled` could be resurrected to `Pending` by `upsert_message`'s unconditional `ON CONFLICT DO UPDATE` - fixed in `991faaa` | +| `MessageStateRace.tla` | `MessageStateRaceFixed.cfg` | fix design (**applied in `991faaa`**) | pass - guarding the resurrect-to-Pending write against terminal status closes the race | +| `Take1ChallengeRace.tla` | `Take1ChallengeRace.cfg` | **pre-fix code (historical)** | **fails, by design**: `connector_a` (Take1 vs. Challenge) had *no* margin check anywhere in `validate_timelock_config`; on Regtest the pre-fix shipped value gave a challenger exactly zero reaction margin - fixed in `991faaa` | +| `Take1ChallengeRace.tla` | `Take1ChallengeRaceFixed.cfg` | fix design (**applied in `991faaa`**) | pass - the missing margin check (mirroring Finding 4's floor) was added, closing the gap | +| `MultiActorRace.tla` | `MultiActorRace.cfg` | verification (no bug; still holds under real shipped values) | pass - also confirms `operator_commit`'s margin against the shared `ConnectorF` UTXO (the `OperatorCommitTimeoutTransaction` path, `ConnectorF` leaf 1's second spender) holds, closing a gap where only a scalar Rust check existed | + +Additional standalone tools available in the jar if needed: SANY (parser/type-checker) +via `java -cp tla2tools.jar tla2sany.SANY .tla`, and the PlusCal translator +(used to generate `GraphLifecycleFineGrained*.tla`'s TLA+ body from its PlusCal +algorithm block) via `java -cp tla2tools.jar pcal.trans .tla`. + ## Contributing Contributions are welcome! Please open an issue or submit a pull request for any improvements or bug fixes. \ No newline at end of file diff --git a/audit/TLAPlus-20260710.md b/audit/TLAPlus-20260710.md new file mode 100644 index 000000000..e463707df --- /dev/null +++ b/audit/TLAPlus-20260710.md @@ -0,0 +1,224 @@ +# BitVM Node — Formal Verification Audit (Round 1) + +**Branch:** `audit/round-1` · **Base:** `gc-v2` · **Method:** TLA+ model checking (TLC) · **Status:** all 8 formally-proven findings from this round are now **fixed and verified** in commit [`991faaa`](https://github.com/GOATNetwork/bitvm-node/commit/991faaabdb56c747103e8f1c6d6477c638ccfc4c) ("Dev fix #418", authored independently by a teammate — not applied by this audit). Two smaller, lower-priority adjacent defects noted under Finding 8 remain open. + +## Executive summary + +This audit used TLA+ to formally model the BitVM graph's status bookkeeping (local database state), the peg-out transaction graph's timelock configuration, and — a later round — the Bitcoin transaction graph's shared connectors directly, checking each against explicit safety and liveness properties rather than relying on manual code review alone. **Eight real issues were found**, each backed by a machine-checked counterexample (not a hypothetical), and a verified-correct fix design was produced for each. Three further checks were run and each, for a specific and verifiable reason, found **no** new issue: whether having multiple independent watchtowers/verifiers introduces new problems beyond the single-actor case (Finding 5), whether `GoatTxProcessingStatus` — the last multi-writer-shaped enum in the codebase — has the same class of race as the others (Finding 8), and whether `operator_commit`'s margin against the shared `ConnectorF` UTXO — previously only a scalar Rust assertion — actually holds with real shipped values (Finding 10). + +**Update: all 8 findings are now fixed.** Commit `991faaa`, contributed independently to this branch, applies real fixes for every one of them. This audit's own contribution to that fix landing was zero — the fixes were designed and written by someone else — but every fix was independently re-verified against this audit's own TLA+ models before being accepted as correct: read against the actual diff (not the commit message), and for the two timelock-margin findings (4, 9), re-checked with TLC using the *actual new shipped numbers*, not just the originally-proposed values. Each Finding below is marked **FIXED (commit `991faaa`)** with what the real applied fix looks like. In several cases the shipped fix is more thorough than what this audit's own fix design proposed (see Findings 1, 6 in particular). + +This round covers **every stateful enum (`pub enum *Status`/`*State`) in the codebase** (not just the two originally scoped, `GraphStatus` and `InstanceBridgeInStatus`) **and every shared/bottleneck connector in the Bitcoin transaction graph itself** (not just `ConnectorD`, the original scope) — see "Full-codebase coverage" below for the enum inventory and Finding 9/10 above for the connector sweep. + +All specs, their configs, and instructions to run them yourself live in `node/tla/`; see the root `README.md`'s "Formal verification (TLA+)" section for the full spec/config table and setup steps. This document is the narrative report: what was found, why it matters, and what fixing it required. + +**A note on `node/tla/`'s current state**: the bug-reproduction `.cfg`/`.tla` pairs (e.g. `GraphLifecycle.cfg`, `Take1ChallengeRace.cfg`) are deliberately left modeling the *pre-fix* code as a permanent historical record — CI (`.github/workflows/ci.yml`) still runs them every time and prints their reproduction command into the job summary, but a bug config correctly still failing (i.e. still reproducing its frozen historical counterexample) no longer fails the job itself. Only a bug config *unexpectedly passing* fails CI, since that would mean either the underlying guard was silently removed again, or the spec itself stopped demonstrating what it claims to. The repo's CI is therefore expected to show `tla-plus` as green now that the real Rust code is fixed — see that job's own comments for the full reasoning. + +--- + +## Methodology + +- **Tool**: [TLA+](https://lamport.azurewebsites.net/tla/tla.html) / TLC, the model checker — not a test suite with example inputs, but an exhaustive search over every reachable combination of the modeled actions, including orderings a human wouldn't think to test by hand. +- **Ground truth discipline**: every spec is built from the actual Rust source and, where relevant, the external `goat`/`bitvm` transaction-graph crate — not from documentation or assumption. Several places where this session's own hand-reasoning turned out to be wrong (see Findings 1b and 4) were caught specifically *because* the model was checked mechanically instead of trusted by argument. +- **Every finding follows the same evidence pattern**: (1) build the model from verified ground truth, (2) get a real TLC counterexample using actual code/config values — not an injected toy case, (3) design a fix, (4) prove the fix closes the gap by re-running TLC, (5) as an added check, re-inject the original bug into the *fixed* model and confirm TLC still catches it (i.e. the fix's own proof isn't a rubber stamp). +- **Scope boundary**: these specs model the *local node's* status bookkeeping and the *timelock arithmetic* of the transaction graph. They do not model Bitcoin consensus itself (a single-spend UTXO is trusted as a primitive, not re-derived) and, before Finding 5, did not model multiple independent watchtowers/verifiers as separate actors. + +--- + +## Findings + +### Finding 1 — `Graph.status` race between two uncoordinated writers · **FIXED (commit `991faaa`)** + +**Severity: data-integrity (not fund-custody).** Bitcoin's UTXO model guarantees the *actual* on-chain outcome (which of take1/take2/disprove really happened) is single and final regardless of this bug — but the *local node's own record* of that outcome can become wrong. + +`Graph.status` is written from two independently-scheduled places with no coordination between them: +- `scan_graph_chain_state` (`node/src/utils.rs:1328-1681`) — derives status by polling Bitcoin. +- The GoatChain L2-event watcher (`node/src/scheduled_tasks/event_watch_task.rs:392-502`) — writes `OperatorDataPushed`/`OperatorTake1`/`OperatorTake2`/`Disprove` directly. + +Both go through `StorageProcessor::update_graph` (`crates/store/src/localdb.rs:1290-1297`), a raw `UPDATE graph SET status = ?` with **no guard** on the row's current status. A stale or replayed event from either subsystem can silently overwrite a terminal status — e.g. reverting a recorded `Disprove` (evidence an operator cheated) back to an earlier status, or flipping between the two mutually-exclusive payout outcomes `OperatorTake1` and `OperatorTake2`. + +**Proof**: `node/tla/GraphLifecycle.tla`, config `GraphLifecycle.cfg`. Counterexample (3 states): `OperatorPresigned` → `OperatorTake1` (a `WithdrawHappyEvent`) → `OperatorDataPushed` (a stale, replayed `PostGraphDataEvent`). A separate run finds the sharper `OperatorTake1 → OperatorTake2` flip in 2 steps. + +**Verified fix design**: fold the guard into the `UPDATE` statement itself — `WHERE status IN (...)` — so the check and the write happen as one atomic database statement instead of a read-then-decide-then-write in application code. Proven in `GraphLifecycleFixed.cfg` (passes all safety and liveness properties). The design needs **two** guard clauses, not one — see Finding 1b for why. + +**What actually shipped**: the old `update_graph_status` is gone entirely, replaced by `StorageProcessor::transition_graph_status` (`crates/store/src/localdb.rs`), doc-commented with exactly this audit's central lesson: *"The conditional UPDATE is the authority check. The follow-up read only distinguishes an idempotent replay from a stale event; it never decides whether a write is permitted."* The guard is a new `GraphStatus::allowed_transition_from(self, source: GraphStatusSource) -> &'static [GraphStatus]` (`crates/store/src/schema.rs`) — a real predecessor table, keyed by *which* writer is asking (`GraphStatusSource::{Definition, GoatEvent, ChainReconcile}`), folded into the same `UPDATE` via `and_where_in("status", &allowed_from, false)`. Verified directly against the transition table: every terminal status is deliberately absent from every `allowed_from` set (the doc comment says so explicitly — "Closed states are deliberately absent from all sets"), correctly implementing absorbing terminals; `OperatorDataPushed`'s allowed predecessors under both `GoatEvent` and `ChainReconcile` correctly exclude `PreKickoff`/`OperatorKickOff` — the exact Guard 2 regression this finding's own fix design required. This is a materially more thorough design than what was proposed here (per-writer-source predecessor tables vs. one shared guard) and it checks out. All `event_watch_task.rs` call sites route through it via a shared `apply_gateway_graph_status` helper — zero remaining raw `update_graph`/`upsert_graph` calls in that file. + +### Finding 1b — a naive version of the Finding 1 fix is itself unsafe · **FIXED (commit `991faaa`, same fix as Finding 1)** + +While designing the Finding 1 fix, a first draft that checked the guard via a plain `SELECT` before the `UPDATE` (rather than folding it into the `UPDATE`'s `WHERE` clause) was modeled explicitly at per-statement granularity — exposing the read and the write as two separate steps, matching how a real read-then-write actually executes with a yield point in between. TLC found this naive version is **still unsafe**: another writer's full read-decide-write can complete inside the gap between the first writer's read and its own write. + +**Proof**: `node/tla/GraphLifecycleFineGrained.tla` (PlusCal-based, exposes the gap explicitly) vs. `node/tla/GraphLifecycleFineGrainedFixed.tla` (collapses the guard-check-and-write into one atomic step, matching a real single SQL statement). The unguarded/naive version fails; the atomic version passes across 132 reachable states with 7-way branching per step. + +**Implication for implementation**: the Finding 1 fix must be a single `UPDATE ... WHERE status IN (...)` statement. A `SELECT` followed by an `UPDATE` — even with correct guard logic — is not sufficient, regardless of how "obviously correct" the guard looks. + +### Finding 2 — `Instance.status` can regress past `Presigned` · **FIXED (commit `991faaa`)** + +**Severity: same class as Finding 1**, different entity. `Instance.status` (`InstanceBridgeInStatus`) has **no** terminal-status concept anywhere in the codebase (unlike `GraphStatus::is_closed()`). `store_graph` (`node/src/utils.rs`) and the graph-status-guard code both write `InstanceBridgeInStatus::Presigned` unconditionally as a side effect of a graph reaching `CommitteePresigned`, reachable from independent P2P-message and chain-rescan paths with no coordination between them. + +**Proof**: `node/tla/InstancePresigned.tla`. Bug config (`InstancePresignedBug.cfg`) finds a 4-state regression: `Early → Presigned → Advanced → Presigned` — an instance already past `Presigned` (e.g. at `RelayerL1Broadcasted`) gets silently reverted. + +**Verified fix design**: same atomic-CAS pattern as Finding 1 (`InstanceUpdate::only_if_status_in`, guard the specific regression). Proven in `InstancePresignedFixed.cfg`. + +**What actually shipped**: the dual-writer pattern was eliminated rather than merely guarded — `Presigned` is now written from exactly one place, `try_transition_instance_to_presigned` (`node/src/utils.rs`), gated on a genuine quorum check (`has_required_presigned_graphs`) before even attempting the write, and using `update_instance_status_if_current` (`crates/store/src/localdb.rs`) — a real single-value SQL CAS: `UPDATE instance SET status = ? WHERE instance_id = ? AND status = ?`. Stricter than this finding's own proposed guard (a single exact expected predecessor, not a small allowed set), and correctly closes the regression. + +### Finding 3 — `instance_window_expiration_monitor`: stale-read TOCTOU with a full-row upsert · **FIXED (commit `991faaa`)** + +**Severity: data-integrity + possible wrong-decision risk.** `instance_window_expiration_monitor` (`node/src/scheduled_tasks/instance_maintenance_tasks.rs:211-271`) batch-reads a page of instances once, then per-instance awaits an RPC call (`gateway_get_pegin_data`) before merging results and doing a **full-row `upsert_instance`**. For batch position *k*, the snapshot is stale by the sum of *k* prior RPC latencies. A concurrent committee-response landing in that window (via `handle_committee_response_events`, on an independent 5-second scheduler tick) gets silently dropped by the full-row overwrite — and because the `CommitteesAnswered` vs. `NoEnoughCommitteesAnswered` decision is computed from that same stale snapshot, an instance that actually reached quorum can be incorrectly marked as not having reached it. + +**No TLA+ model was built for this one** — it's a classic read-modify-write staleness bug, not a multi-writer race with clean state-machine structure, and was addressed with a narrower code-level fix (re-read the instance immediately before the decision, re-validate the precondition) rather than a formal proof. + +**What actually shipped**: `instance_window_expiration_monitor` now opens `local_db.start_immediate_transaction()` (SQLite `BEGIN IMMEDIATE`, taking the write lock upfront) and re-reads the instance with `find_instance` *inside* that transaction, right before making the decision and writing it — with an explicit comment: *"Re-read while holding SQLite's write lock, then make the decision and its update in the same short transaction so a late committee response cannot be overwritten by the stale page snapshot."* This is exactly the fix direction described above, verified by reading the function directly. + +### Finding 4 — timelock margin gaps in `validate_timelock_config` · **FIXED (commit `991faaa`)** + +**Severity: protocol-parameter safety.** `validate_timelock_config` (`crates/bitvm-gc/src/timelocks.rs:69-117`) checks a chain (`watchtower_challenge < operator_ack < operator_commit < connector_f`) that is correct and already enforced — confirmed by cross-referencing that all four are measured from the same shared clock (`WatchtowerChallengeInit`'s confirmation). But two things were **not** checked: + +1. **No absolute, network-aware floor.** Every field is checked non-zero, but nothing is checked against the network's actual block time. `connector_a` (take1) in particular has no relative check to anything at all — this observation was followed up on directly later in this round and turned into a full finding with its own TLA+ proof and real counterexample; see Finding 9. +2. **A Δ-blind margin comparison.** `prover_connector ≤ connector_d` compares two timelocks with genuinely different clock starts (`connector_d` from `OperatorAssert`, `prover_connector` from `VerifierAssert`, which can only confirm *after* `OperatorAssert`). The check as written accepts equality, with no accounting for that real-world gap. + +**Proof this is exploitable, not just theoretical**: `node/tla/Take2DisproveRace.tla`, modeling the actual UTXO race (`Take2Transaction` spends `ConnectorD` leaf 0, `DisproveTransaction` spends leaf 1 — confirmed by reading `goat/src/transactions/{take2,assert}.rs` directly). Running it against the **actual shipped** testnet4 config found a real boundary case on the first try, no injected bug needed: `prover_connector(22) + min_reaction_blocks(12) = 34 = connector_d(34)` — Disprove's and Take2's earliest-spendable heights land on the **exact same block**, making the outcome a coin-flip on mempool/miner ordering rather than a guaranteed win for the honest Disprove path. + +**Verified fix design**: (a) a network-aware absolute floor (~1 hour of reaction time, derived from `estimated_block_interval_secs`, exempting the pure test networks Signet/Regtest), (b) a *strict* margin requirement (`prover_connector + min_margin < connector_d`, not `≤`), (c) bumping testnet4's `connector_d` from 34 to 35 to restore a valid margin. All three verified in `Take2DisproveRace.cfg` (checked across all 4 networks and a wide range of real-world confirmation-delay values). + +**A caution about this specific finding**: the first draft of the fix used a *non-strict* margin check, which TLC caught failing on the very first run using the real testnet4 value — i.e. the initial fix attempt for Finding 4 itself had the same class of boundary bug it was trying to fix. This is documented as evidence for why every fix in this audit was re-verified by TLC rather than accepted on the strength of the reasoning behind it. + +**What actually shipped**: `min_reaction_blocks(network)` is now a real function, *computed* per-network from a 1-hour floor (`(MIN_REACTION_SECS + interval - 1) / interval`) for Bitcoin/Testnet4 and hardcoded to 1 for Signet/Regtest — independently re-derived by whoever wrote the fix, and it lands on **exactly** the same numbers this audit's `MinReactionBlocks` table used (Bitcoin 6, Testnet4 12, Signet 1, Regtest 1). `ensure_lte` became `ensure_reaction_margin`, using `left.saturating_add(min_margin) >= right` to bail — a real strict-margin check, not just `<`/`<=` swapped. The shipped numbers moved further than this audit's own minimal proposal: Testnet4 `connector_d` went 34→**40** (not just →35) and `prover_connector` moved 22→20, for a real margin of 8 blocks rather than the bare +1 this audit proposed; several other Testnet4 fields (`watchtower_challenge`, `operator_ack`, `operator_commit`, `connector_f`) were retuned too. **Re-verified, not assumed**: `Take2DisproveRace.tla`/`MultiActorRace.tla` were re-run with TLC against these actual new numbers (not the originally-proposed ones) — `ShippedTimelocks.tla` now carries them — and every property still holds: 14,424 and 1,459,745 states respectively, zero violations. + +### Finding 5 (verification, not a bug) — multiple independent watchtowers/verifiers + +Before generalizing Finding 4's fix, it was necessary to check whether the real protocol requires a **quorum** of watchtowers/verifiers to catch fraud, or whether **any single one** suffices regardless of the others — getting this wrong would silently invalidate the single-actor margin analysis above. Ground truth (read directly from `goat/src/connectors/watchtower_connectors.rs`, `connector_e.rs`, `connector_f.rs`, `assert_connectors.rs`, `connector_d.rs`, and `node/src/utils.rs:1264-1326`) confirmed: **true 1-of-N** for both roles. Any single watchtower an operator fails to acknowledge, or any single verifier's fraud assertion the operator fails to rebut, permanently denies the operator's `Take2` claim via a shared bottleneck connector (`ConnectorF` / `ConnectorD`) — first-confirmed-wins UTXO semantics, no counting or quorum anywhere in the code. + +`node/tla/MultiActorRace.tla` modeled N=2 independent watchtowers and M=2 independent verifiers, each choosing their own timing within their protocol-allowed window, across all 4 networks — **1,618,897 combinations checked, zero violations.** Re-injecting Finding 4's exact testnet4 boundary bug into this multi-actor model was caught immediately (with a counterexample where *only one* of the two verifiers acts, confirming the check is genuinely per-actor and not an artifact of needing both to coincide). + +**Conclusion**: the single-actor margin analysis in Finding 4 generalizes correctly to any number of watchtowers/verifiers. No additional fix is needed for multiplicity itself. + +### Finding 6 — `InstanceBridgeOutStatus` can be resurrected to `Initialize` after reaching a terminal outcome · **FIXED (commit `991faaa`)** + +**Severity: same class as Finding 1/2** — local bookkeeping, not direct fund loss, but capable of triggering redundant or contradictory downstream actions against an already-resolved bridge-out (withdraw) instance. + +`InstanceBridgeOutStatus` (`Initialize`/`Claim`/`Timeout`/`Refund`, `crates/store/src/schema.rs:223-229`) is written from **three** independently-scheduled, uncoordinated places, none of which share a transaction spanning read+decide+write: + +- The RPC-service task (`node/src/rpc_service/handler/bitvm_handler.rs:308-368`, `bridge_out_init_tag`) — a stale-read-then-full-row-`upsert_instance`, sets `Initialize` unconditionally as part of a full-row overwrite. +- The GoatChain L2-event watcher (`node/src/scheduled_tasks/event_watch_task.rs:634-669,756-763,810-815`) — a 5-second tokio task, unconditional targeted `update_instance` on `SwapClaimEvent`/`SwapRefundEvent`, sets `Claim`/`Refund` with **no status precondition** (`InstanceUpdate`'s `WHERE` clause is only `hex(instance_id)=?` — confirmed via `crates/store/src/localdb.rs` that no `with_only_if_status_in`-style guard exists anywhere in the codebase for this entity). +- The maintenance task (`node/src/scheduled_tasks/instance_maintenance_tasks.rs:482-517`, `instance_bridge_out_monitor`) — a 10-second tokio task, batch-reads a stale snapshot then per-row does an unconditional targeted `update_instance` to `Timeout` with no re-check at write time. + +All three tasks run independently and concurrently — confirmed via `main.rs:191-274`'s task topology (RPC handler always-on, watch-event loop every 5s, maintenance loop every 10s). A stale full-row upsert from the RPC path landing after a `Claim`/`Timeout`/`Refund` has already been recorded silently resets the instance back to `Initialize`, which the maintenance task will then treat as still-pending and act on again. + +**Proof**: `node/tla/InstanceBridgeOutRace.tla`, config `InstanceBridgeOutRace.cfg`. Real 3-state counterexample: `Initialize → Claim → Initialize` — exactly the RPC stale-upsert-clobbers-a-recorded-Claim scenario. Modeled the same way `GraphLifecycle.tla` models its race: every writer unconditional on the current status, matching the real, verified behavior — not a simplification of it. + +**Verified fix design**: the same atomic-CAS pattern as Findings 1 and 2 — fold `status \notin {Claim, Timeout, Refund}` into each writer's `UPDATE`/upsert `WHERE` clause. Proven in `InstanceBridgeOutRaceFixed.cfg`. + +**What actually shipped**: `InstanceUpdate` (`crates/store/src/localdb.rs`) gained a real `only_if_status_in: Option>` field, `with_only_if_status_in(...)`, wired into `get_query_builder` via `QueryBuilder::and_where_in("status", statuses, false)` — the exact atomic-CAS mechanism this finding's fix design called for. All three sites were fixed, and the shipped fix goes further than proposed at each one: `bridge_out_init_tag` and the SwapClaim/SwapRefund handlers both switched from full-row `upsert_instance` (no `WHERE` clause possible on `INSERT OR REPLACE`, the actual root cause) to targeted, guarded `update_instance` calls, plus two additional guards this audit didn't flag (`with_only_if_is_bridge_in(false)`, `with_only_if_goat_tx_hash(...)` for idempotency against replayed events) and a real atomic `insert_instance_if_absent` (`INSERT ... ON CONFLICT(instance_id) DO NOTHING`) closing a related instance-creation race. `instance_bridge_out_monitor` now applies the guard unconditionally on every write from that function, not just the `Timeout`-setting branch. + +### Finding 7 — `MessageState` can be resurrected from `Cancelled` to `Pending`, re-dispatching a moot message · **FIXED (commit `991faaa`)** + +**Severity: protocol-hygiene / data-integrity**, lower direct impact than Findings 1/2/6 — this does not corrupt a fund-relevant status field, but it can cause a peer-facing protocol message about an already-finalized graph to be silently re-sent. + +Unlike Findings 1/2/6, one side of this race **is** correctly guarded: `update_messages_state_by_business_id` (`crates/store/src/localdb.rs:1811-1841`) performs a real compare-and-set — `UPDATE ... WHERE business_id=? AND state='Pending'` — called from `node/src/scheduled_tasks/event_watch_task.rs`'s `handle_withdraw_paths_events`/`handle_withdraw_disproved_events` to bulk-cancel any still-`Pending` message for a graph once that graph reaches a closed on-chain status (`OperatorTake1`/`OperatorTake2`/`Disprove`). + +The bug is on the *other* side: `upsert_message` (`node/src/utils.rs:3703-3744`), called by the generic "defer/retry this P2P message" primitive `push_local_unhandled_messages` (`node/src/utils.rs:1698-1717`, ~30 call sites across `node/src/handle.rs`) with `is_update=true`, performs `INSERT ... ON CONFLICT(message_id) DO UPDATE SET state=excluded.state`. An `ON CONFLICT DO UPDATE` upsert has no `WHERE` clause to guard with — so a message the system just administratively marked `Cancelled` (because its graph already closed) can be silently resurrected to `Pending` the next time any handler in the swarm-message-processing path calls a retry/defer on it, entirely unrelated to the cancellation. + +**Proof**: `node/tla/MessageStateRace.tla`, config `MessageStateRace.cfg`. Real 3-state counterexample: `Pending → Cancelled → Pending`, confirming the resurrection is reachable, not merely theoretical. + +**Verified fix design**: guard the resurrect-to-`Pending` write the same way — `status \notin {Cancelled}` folded into the upsert's effective condition (e.g. an `ON CONFLICT ... WHERE message.state != 'Cancelled'` clause, or a read-before-upsert with the CAS pattern used elsewhere). Proven in `MessageStateRaceFixed.cfg`. + +**What actually shipped**: `WHERE message.state != 'Cancelled'` added to `upsert_message`'s `ON CONFLICT(message_id) DO UPDATE SET ...` clause — verbatim the fix design proposed here. One implementation detail worth noting: `upsert_message` was also switched from the compile-time-checked `sqlx::query!` macro to the runtime `sqlx::query`, sidestepping the `.sqlx` query-cache regeneration this audit flagged as a friction point for landing this specific change. + +### Finding 8 (verification, not a bug) — `GoatTxProcessingStatus` has no equivalent race + +The last remaining multi-writer-shaped candidate (written from `node/src/scheduled_tasks/event_watch_task.rs`, `graph_maintenance_tasks.rs`, and `instance_maintenance_tasks.rs`) was checked and found **not** to share the Finding 1/2/6/7 race pattern, for a specific structural reason rather than luck: every write to `GoatTxProcessingStatus::Processed` is gated behind an **on-chain proof requirement** for `proceedWithdraw` (confirmed via `crates/client/src/goat_chain/goat_adaptor.rs:178`) combined with the `is_processing_gateway_history_events` mutex-like gate (`node/src/scheduled_tasks/mod.rs:82-85`), which serializes the competing writers by construction rather than relying on a database-level guard. No TLA+ model was built for this one since there is no race to demonstrate — the causal ordering argument is the finding. + +Confirmed directly against the L2 contract itself (`Gateway.sol`, [`KSlashh/bitvm-L2-contracts@2173b92`](https://github.com/KSlashh/bitvm-L2-contracts/tree/gc-v2), the `gc-v2`-tracking fork): `proceedWithdraw` (`Gateway.sol:474-498`) requires a Merkle-proven kickoff tx (`_verifyMerkleInclusion`) and reverts unless `withdrawData.status == WithdrawStatus.Initialized`, and every withdraw-finalizing function (`proceedWithdraw`, `finishWithdrawHappyPath`, `finishWithdrawUnhappyPath`, `finishWithdrawDisproved`) is `onlyCommittee`-gated. `finishWithdrawDisproved` (`Gateway.sol:521-534`) additionally reverts with `AlreadyDisproved()` if `withdrawData.status == WithdrawStatus.Disproved` already — a real, correctly-guarded terminal state on the contract's *own* on-chain bookkeeping. This is worth stating explicitly: it confirms the pattern common to every race finding in this report (1, 2, 6, 7) is specifically that the *local node's* mirror of on-chain state can drift from a well-guarded source of truth — not that the on-chain/L2 state itself is unguarded. The L2 contract doing its own job correctly is exactly why these are data-integrity findings about the node's bookkeeping, not fund-custody findings about the protocol's on-chain enforcement. + +Two **adjacent, non-race** defects were found while establishing this and are noted here as lower-priority follow-ups, not part of the races proven elsewhere in this report. **Neither appears to be fixed in commit `991faaa`** (checked directly against the current code, not assumed): + +1. **Sticky-`Processed` bug on withdrawal cancel+reinit.** The merge guard originally at `crates/store/src/localdb.rs:2272-2274` only protected the `Processed` status itself from being overwritten. That specific function (`update_goat_tx_record_processing_status`) has since been simplified to a plain unconditional `UPDATE goat_tx_record SET processing_status = ? WHERE instance_id=? AND graph_id=? AND tx_type=?` with no merge-guard logic at all — the underlying cancel+reinit correctness question is unresolved either way; the original narrow bug and the newer unconditional-write shape both remain **open**. +2. **A same-file self-race in the history-catchup task spawner** (`node/src/scheduled_tasks/event_watch_task.rs`, now around line 1322) requiring a 10-minute stall to trigger, gated by `LOAD_HISTORY_EVENT_NO_WOKING_MAX_SECS=600s` (`node/src/env.rs:138`, value unchanged) — the spawn-guard logic looks structurally the same as originally found. **Still open.** + +### Finding 9 — `connector_a` (Take1 vs. Challenge) has no margin check at all · **FIXED (commit `991faaa`)** + +**Severity: protocol-parameter safety, same class as Finding 4** — this is the first check in the peg-out graph's dispute timeline, not a database-bookkeeping issue. + +This round extended the audit from local-database status races to the Bitcoin transaction graph itself: every shared/bottleneck Taproot connector (a UTXO with 2+ leaves spendable by different, competing transactions) was mapped from the `goat` crate source (checkout `e369b2a`, the same commit `Take2DisproveRace.tla` already cites), looking for the same shape of finding Finding 4 found on `ConnectorD`. + +`ConnectorA`'s output is created by `KickoffTransaction` (`goat/src/transactions/kickoff.rs:67-71`) and has two leaves: leaf 0 (operator key + CSV `connector_a`, `connectors/connector_a.rs:37-46`) spent by `Take1Transaction` — the operator's uncontested fast-exit path — and leaf 1 (n-of-n committee key, **no CSV at all**, `connector_a.rs:48-54`) spent by `ChallengeTransaction` using `SinglePlusAnyoneCanPay` (`transactions/challenge.rs:56-57,93`) so any third-party challenger can add their own fee input and force it through the instant fraud is noticed. Because both leaves share the same clock root (Kickoff's confirmation) and Challenge itself has zero wait, `connector_a`'s value **is** the entire real-world window a challenger has to detect fraud and get a Challenge transaction confirmed before Take1's CSV matures. + +`crates/bitvm-gc/src/timelocks.rs`'s `validate_timelock_config` (69-101) checks `connector_a` is merely nonzero (73-81) but — unlike `prover_connector`, `watchtower_challenge`, `operator_ack`, and `operator_commit`, every one of which appears in an `ensure_lt`/`ensure_lte` comparison (91-99) — `connector_a` never appears in any cross-field check. Nothing in the codebase enforces that it leaves a challenger enough reaction time, on any network. + +**Proof this is exploitable, not just theoretical**: `node/tla/Take1ChallengeRace.tla`, config `Take1ChallengeRace.cfg`, checked against the actual shipped per-network values (Bitcoin 144, Testnet4 16, Signet 6, Regtest 1) and reusing `Take2DisproveRace.tla`'s already-established `MinReactionBlocks` policy floor (Bitcoin 6, Testnet4 12, Signet 1, Regtest 1). TLC finds a real counterexample on the first run, no injected bug needed: on Regtest, `connector_a(1) = MinReactionBlocks(1)` — the challenger has *exactly zero* margin, the same "coin-flip boundary" shape as the original `ConnectorD`/testnet4 finding. Bitcoin, Testnet4, and Signet all currently satisfy a strict margin (138/4/5 blocks respectively) — Regtest is the only network that actually fails today — but that is beside the real point: **no mechanism stops a future value from being unsafe on any network**, since the check simply doesn't exist. + +**Verified fix design**: add an `ensure_gt("connector_a", config.connector_a, "min_reaction_blocks", ...)`-style check to `validate_timelock_config`, mirroring Finding 4's floor. Bumping Regtest's shipped value from 1 to 2 is sufficient to satisfy it with everything else unchanged. Proven in `Take1ChallengeRaceFixed.cfg`. + +**What actually shipped**: `ensure_gt("connector_a", config.connector_a, "min_reaction_blocks", min_reaction_blocks(network))` — added to `validate_timelock_config` essentially verbatim, and `NODE_REGTEST_TIMELOCK_CONFIG.connector_a` bumped 1→2, exactly the minimal fix proposed. Re-verified by updating `Take1ChallengeRace.tla`'s `ConnectorA` (kept as a historical record of the pre-fix value, unchanged) against the real `min_reaction_blocks` values and confirming `Take1ChallengeRaceFixed.cfg` (Regtest=2, matching the real shipped number) still passes — it does. `node/tla/Take1ChallengeRace.tla`'s header now notes this history explicitly. + +### Finding 10 (verification, not a bug) — `operator_commit`'s margin against the shared `ConnectorF` UTXO, now formally confirmed + +While mapping every bottleneck connector for Finding 9, `ConnectorF` (`connectors/connector_f.rs`) turned out to have a structural detail Finding 5's `MultiActorRace.tla` didn't fully capture: leaf 1 (the "committee blocks Take2" leaf) has **two** alternative spenders, not one — `OperatorChallengeNackTransaction` (already covered by `NackAlwaysBeatsTake2ViaF`) **and** `OperatorCommitTimeoutTransaction` (`transactions/watchtower_challenge.rs:714-747`, jointly spending `ConnectorE` leaf 1 as its other input). The Rust side already enforces `operator_commit < connector_f` (`timelocks.rs`'s `ensure_lt`), but unlike the `operator_ack`/Nack pairing, that specific comparison had never been independently confirmed with real shipped values at the TLA+ level — it existed only as an unverified scalar Rust assertion. + +`MultiActorRace.tla` was extended (not a new file — same clock-root reasoning, same module, `operator_commit` isn't a multi-actor quantity so no new `VARIABLE` was needed) with `CommitTimeoutAlwaysBeatsTake2ViaF == OperatorCommit[net] < ConnectorF[net]`, checked against real shipped values (`operator_commit`: Bitcoin 432, Testnet4 58, Signet 18, Regtest 3). **Result: holds on all 4 networks** (margins of 144/12/6/1 blocks respectively), re-verified across the full existing 1,618,897-state space in `MultiActorRace.cfg` alongside the properties Finding 5 already established. No new issue — this closes a real gap in *verification coverage*, not a bug in the protocol. + +**Other connectors reviewed, no additional TLA+ model warranted:** +- **`ConnectorZ`** (`PegInConfirm` vs `PegInRefund`, protecting end-user escrowed BTC) has a structurally different shape from the fraud-detection races above: the committee's leaf (immediate, no CSV) never expires, and the user's refund leaf only becomes *additionally* available after a timeout — there is no attacker racing a short CSV against a defender's reaction window the way Finding 4/9 do, so the `MinReactionBlocks`-margin pattern doesn't apply. Whether the underlying escrow design itself is sound is a different, non-margin question outside this round's scope. +- **`AckConnector`'s leaves 0 and 1** (of its 3 leaves; leaf 2 is already covered by Findings 5/10) carry no CSV of their own — confirmed by reading `connectors/watchtower_connectors.rs:96-159` directly — so there is no timelock arithmetic between them to check; whichever is broadcast first wins on ordinary mempool terms, not a margin race. +- **`ConnectorD`'s third leaf** (`PubinDisprove`, `connector_d.rs:20-22,80-88` — `ConnectorD` has 3 leaves, not the 2 `Take2DisproveRace.tla`'s original header implied) carries no CSV, so it can only ever be at least as fast as the already-proven-safe `Disprove` path, never slower — noted in that file's header as a documentation-completeness fix, not a new margin model, since it cannot introduce a new exploitable case. + +--- + +## Full-codebase coverage + +This round's mandate was to check every stateful enum in the codebase, not just the two originally in scope. The methodology: enumerate every `pub enum *Status`/`*State`, grep every `.rs` file that writes to each, and deep-dive (research + TLA+) any type written from 2+ independent files/tasks. `GraphStatus`, `InstanceBridgeInStatus`, `InstanceBridgeOutStatus`, and `MessageState` all met that bar and are covered above (Findings 1/1b, 2, 6, 7). `GoatTxProcessingStatus` met the bar but was found safe (Finding 8). The remaining six were triaged with lighter-weight, targeted evidence review — each is written from effectively one call site/owner or is inherently request-scoped, so no plausible concurrent-writer race exists: + +| Type | Why it's low-risk | +|---|---| +| `WatchContractStatus` | Single owner: only the watchtower-contract lifecycle task writes it; no second writer path exists. | +| `ProofState` | Owned end-to-end by the proof-builder pipeline for a single circuit run; not shared across tokio tasks. | +| `SimpleChallengeSubStatus` | Derived/computed per-read from other already-guarded state, not itself an independently-written column. | +| `VerifierChallengeStatus` | Written only by the per-verifier challenge-processing path for that verifier's own slot; no cross-verifier or cross-task writer. | +| `PeginStatus` | Single writer path in the pegin request handler; terminal transitions are request-response scoped, not background-task driven. | +| `WithdrawStatus` | Same shape as `PeginStatus` — single request-scoped writer, no independent background writer competing for the same row. | + +None of these were modeled in TLA+; the absence of a second independent writer is itself the finding, and is falsifiable by grep (re-run the same "how many `.rs` files write to this enum" search if the code changes). + +--- + +## Documentation drift found during this audit + +Independent of the formal-verification findings above, cross-checking `node/README.md`'s state-machine diagrams against the actual code (rather than trusting the diagrams as ground truth for the TLA+ models) surfaced three places where the documentation had drifted from the implementation: + +1. **`Obsoleted` is not actually terminal.** The README's diagram showed it as a dead end; `scan_graph_chain_state` (`node/src/utils.rs:1418-1434`) can resurrect an `Obsoleted` graph into `OperatorKickOff` or `Skipped` if a kickoff tx is later observed on-chain. +2. **A direct `OperatorKickOff → Disprove` edge** (guardian disprove, bypassing `Challenge` entirely) exists in code (`node/src/utils.rs:1448-1461`) but was missing from the README diagram. +3. **The `ChallengeSubStatus` struct's documented shape didn't match the real struct at all** — the README described three single-value enums; the actual struct (`node/src/scheduled_tasks/graph_maintenance_tasks.rs:52-58`) is a per-watchtower `Vec` plus a per-verifier `Vec`, and the documented enums don't exist in the codebase. + +These were fixed directly in `node/README.md` as documentation corrections (not reverted, since they're not behavioral code changes) — see that file's Graph State Machine section for the corrected diagrams. + +--- + +## Recommendations + +1. ~~Apply the verified fixes.~~ **Done, as of commit `991faaa`**, for all 8 formally-proven findings (1, 1b, 2, 3, 4, 6, 7, 9) — see each finding's "What actually shipped" note above. This was not this audit's own work; it was verified against the real diff after the fact. +2. **Fix the two adjacent, lower-priority defects noted under Finding 8** (sticky-`Processed` on withdrawal cancel+reinit; the history-catchup task spawner's same-file self-race) — checked directly against `991faaa`, **neither is fixed yet**. Still real, narrow bugs worth a small patch; still no formal model needed. +3. **Keep `node/tla/`'s value-carrying specs in sync with `crates/bitvm-gc/src/timelocks.rs` going forward** — this already happened once and needed a manual catch-up: `ShippedTimelocks.tla` held stale pre-fix numbers for several hours after `991faaa` actually shipped different (better) values, because nothing ties the `.tla` constants to the real Rust source automatically. A `cargo test` that reads `timelocks.rs`'s constants and diffs them against the `.tla` file's text (the same pattern this repo used for `tla_model_matches_shipped_timelock_configs` earlier in this round, before it was reverted along with the rest of the applied-fix code) would make this mechanically self-checking instead of relying on someone remembering to look. +4. **Consider extending Finding 5's multi-actor model** if the team wants deeper coverage — e.g. Byzantine actors (a watchtower/verifier actively trying to help the operator, not just staying silent), or N > 2 to rule out any count-dependent effect the N=2 case might not surface. +5. **Adopt a traceability mapping going forward**: a `traceability/*.yaml`-style mapping from every stateful enum's writers to the Rust symbols that touch them would make the "which types have 2+ independent writers" triage in this audit mechanically re-checkable instead of requiring a fresh grep sweep each round. +6. **Keep the tripwire discipline this audit's tooling established.** `node/tla/`'s `.tla` files cite the exact Rust functions they model in reverse-pointer comments; if those functions are edited again, the specs need to be re-verified, not just assumed to still apply. The bug-reproduction configs (e.g. `GraphLifecycle.cfg`) are deliberately kept modeling the *pre-fix* code as a permanent historical record, run every CI build, but only fail the job if one of them unexpectedly starts passing — see root `README.md` and `.github/workflows/ci.yml`'s comments for the full reasoning, and why `tla-plus` is expected to be green on this branch now that the underlying bugs are fixed. +7. **Extend the connector sweep to a structural/topology and value-conservation audit** as a separate follow-up round, if desired. This round's connector sweep (Findings 9/10) covered every *margin/reaction-time* race in the Bitcoin transaction graph; it deliberately did not check whether the constructed transactions' actual wiring (inputs/outputs/amounts) matches the intended graph, whether fees/amounts conserve correctly end-to-end, or whether the Taproot leaf scripts encode the intended authorization — those are different classes of correctness property, better suited to direct code audit or Rust property tests than to TLA+'s margin-arithmetic style. + +--- + +## Spec inventory + +See root `README.md`'s "Formal verification (TLA+)" section for the authoritative, up-to-date table (spec, config, what it models, expected result) and exact run commands. Summary: + +| Spec | Findings it covers | Status | +|---|---|---| +| `GraphLifecycle.tla` | Finding 1 | Fixed in `991faaa`; bug config kept as permanent historical regression check | +| `GraphLifecycleFineGrained.tla` / `GraphLifecycleFineGrainedFixed.tla` | Finding 1b | Fixed in `991faaa`; bug config kept as permanent historical regression check | +| `InstancePresigned.tla` | Finding 2 | Fixed in `991faaa`; bug config kept as permanent historical regression check | +| `Take2DisproveRace.tla` | Finding 4 | Fixed in `991faaa`; `ShippedTimelocks.tla` updated to the real new numbers, re-verified via TLC | +| `MultiActorRace.tla` | Finding 5 | Verification (no bug); still holds under the real new `991faaa` numbers | +| `InstanceBridgeOutRace.tla` | Finding 6 | Fixed in `991faaa`; bug config kept as permanent historical regression check | +| `MessageStateRace.tla` | Finding 7 | Fixed in `991faaa`; bug config kept as permanent historical regression check | +| `Take1ChallengeRace.tla` | Finding 9 | Fixed in `991faaa`; `ConnectorA` kept as historical pre-fix value, `ConnectorAFixed` matches the real shipped value | +| `MultiActorRace.tla` (extended) | Finding 10 | Verification (no bug); still holds under the real new `991faaa` numbers | + +All specs are runnable today: `cd node/tla && java -jar ~/.local/share/tlaplus/tla2tools.jar -config .cfg .tla`. CI (`.github/workflows/ci.yml`, job `tla-plus`) runs the full set on every push/PR against `gc-v2`, and is *expected* to show the bug-reproduction configs still failing — they model the pre-fix code on purpose, as a permanent regression check, not a claim that the bug is still live in shipped code. See that job's own comments and the note at the top of this report. diff --git a/circuits/README.md b/circuits/README.md index a740c67f9..031df4485 100644 --- a/circuits/README.md +++ b/circuits/README.md @@ -1,4 +1,4 @@ -# BitVM2 Circuits +# BitVM Circuits ## Overview @@ -203,7 +203,7 @@ RUST_LOG=info cargo run --package watchtower-proof --bin watchtower-proof -r -- * Simutate a withdraw challenge ```bash -cd crates/bitvm2-ga +cd crates/bitvm-gc cargo test -r test_take2 ``` @@ -242,10 +242,13 @@ export WATCHTOWER_CHALLENGE_INIT_TXID="e7723e03ac97172cf033e40d4b9d9c0e22efa7a41 RUST_LOG=info cargo run --package operator-proof --bin operator-proof -r -- --output "data/operator-proof/output.bin" ``` +The operator proof is consumed directly by the Assert flow. BABE setup binds its static public +input, while the dynamic public input is committed after the watchtower challenge set is known. + * latest-sequencer-commit-txid: the latest publisher's commitment Bitcoin transaction id * header-chain-input-proof: the header chain's proof, input and vk. * commit-chain-input-proof: the commit chain's proof, input and vk. -* included-watchtower: a 256-bit bitmask; each bit flags a valid watchtower. +* included-watchtower: a 256-bit bitmask; each bit flags a valid watchtower inside operator proof. * execution-layer-block-number: the block number that including `proceedWithdraw`(Peg-out) transaction of GOAT Network's execution layer(Geth). * watchtower-challenge-info: list of watchtower's challenge transaction id and compressed public key, i.e: [wachtower_info.json](./data/watchtower/watchtower_info.json). -* watchtower-challenge-init-txid: the watchtower challenge init transaction id in GOAT's BitVM2 graph. +* watchtower-challenge-init-txid: the watchtower challenge init transaction id in GOAT's BitVM graph. diff --git a/circuits/commit-chain-proof/guest/Cargo.lock b/circuits/commit-chain-proof/guest/Cargo.lock index 51593fa10..a96c3e31c 100644 --- a/circuits/commit-chain-proof/guest/Cargo.lock +++ b/circuits/commit-chain-proof/guest/Cargo.lock @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "alloy-primitives" version = "1.4.1" -source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#59313700f710f1373a5b4cdc05c51dc3227d8064" +source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#0d034202c9977acd601399efdd9c30fd1c1a08a0" dependencies = [ "alloy-rlp", "bytes", @@ -1069,6 +1069,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1103,7 +1117,7 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#bcf9397426410acc171ad625416b508dc913be8e" +source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#97b215f80e9bfab9ad6641e41b005b6d8e58f748" dependencies = [ "cfg-if", "ff 0.13.1", @@ -1256,7 +1270,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commit-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1271,7 +1285,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -1282,7 +1296,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -1343,6 +1357,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -1424,7 +1447,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version 0.4.1", @@ -2235,14 +2258,13 @@ dependencies = [ "commit-chain", "sha2 0.10.9", "tracing", - "zkm-verifier", "zkm-zkvm", ] [[package]] name = "guest-executor" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-consensus", "alloy-evm", @@ -2736,7 +2758,7 @@ dependencies = [ [[package]] name = "k256" version = "0.13.4" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#8266b228a39402a0ba68d644b7f26b85b5112fe3" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#6ad84b9b604911c6f5dd353b88c04927d2739a32" dependencies = [ "cfg-if", "ecdsa", @@ -2755,7 +2777,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -2900,7 +2922,7 @@ dependencies = [ [[package]] name = "mpt" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -3157,7 +3179,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p256" version = "0.13.2" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "ecdsa", "elliptic-curve", @@ -3716,7 +3738,7 @@ dependencies = [ [[package]] name = "primeorder" version = "0.13.1" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "elliptic-curve", ] @@ -3735,7 +3757,7 @@ dependencies = [ [[package]] name = "primitives" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-eips", "alloy-genesis", @@ -4986,7 +5008,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -4997,7 +5019,7 @@ version = "0.10.9" source = "git+https://github.com/ziren-patches/RustCrypto-hashes?branch=patch-sha2-0.10.9#dbfdbd088ac7b55e03c0b9eb192278efe35d67dc" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5163,7 +5185,7 @@ dependencies = [ [[package]] name = "substrate-bn" version = "0.6.0" -source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" +source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#0dfeeb1b7bfc21941b6e4964d678abd094a54a6f" dependencies = [ "bytemuck", "byteorder", @@ -5649,6 +5671,14 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "sha2 0.10.9", + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" @@ -6217,8 +6247,8 @@ dependencies = [ [[package]] name = "zkm-core-executor" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6258,8 +6288,8 @@ dependencies = [ [[package]] name = "zkm-core-machine" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6310,8 +6340,8 @@ dependencies = [ [[package]] name = "zkm-curves" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "cfg-if", "curve25519-dalek", @@ -6334,8 +6364,8 @@ dependencies = [ [[package]] name = "zkm-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "proc-macro2", "quote", @@ -6344,8 +6374,8 @@ dependencies = [ [[package]] name = "zkm-lib" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6357,10 +6387,11 @@ dependencies = [ [[package]] name = "zkm-primitives" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -6371,12 +6402,13 @@ dependencies = [ "p3-symmetric", "serde", "sha2 0.10.9", + "tracing", ] [[package]] name = "zkm-recursion-core" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "backtrace", "ff 0.13.1", @@ -6412,8 +6444,8 @@ dependencies = [ [[package]] name = "zkm-stark" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -6454,8 +6486,8 @@ dependencies = [ [[package]] name = "zkm-verifier" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6484,10 +6516,11 @@ dependencies = [ [[package]] name = "zkm-zkvm" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "cfg-if", "getrandom 0.2.17", "lazy_static", diff --git a/circuits/commit-chain-proof/guest/Cargo.toml b/circuits/commit-chain-proof/guest/Cargo.toml index 395813bdc..55cd39c7c 100644 --- a/circuits/commit-chain-proof/guest/Cargo.toml +++ b/circuits/commit-chain-proof/guest/Cargo.toml @@ -11,9 +11,7 @@ bincode = "1.3.3" commit-chain = { path = "../../../crates/commit-chain" } # Ziren -zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } -zkm-verifier = { git = "https://github.com/ProjectZKM/Ziren" } -#zkm-verifier = { path = "../../../../Ziren/crates/verifier" } +zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7", features = ["verify"] } #zkm-zkvm = { path = "../../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging @@ -37,3 +35,6 @@ alloy-primitives-v1-1-2 = { git = "https://github.com/ziren-patches/core.git", p alloy-primitives-v1-4-1 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.4.1" } secp256k1-v0-29-1 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.29.1" } #secp256k1-v0-30-0 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.30.0" } + +[features] +imm-wrap-vk = ["zkm-zkvm/imm-wrap-vk", "commit-chain/imm-wrap-vk"] diff --git a/circuits/commit-chain-proof/host/Cargo.toml b/circuits/commit-chain-proof/host/Cargo.toml index 53e946ce4..c2c48a0c1 100644 --- a/circuits/commit-chain-proof/host/Cargo.toml +++ b/circuits/commit-chain-proof/host/Cargo.toml @@ -25,6 +25,7 @@ tendermint-light-client-verifier = { workspace = true, default-features = false, ] } proof-builder.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 72963ff6b..6a32f43b1 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -4,8 +4,8 @@ use commit_chain::*; use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; use std::str::FromStr; use zkm_sdk::{ - HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, - include_elf, + HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofKind, ZKMProofWithPublicValues, + ZKMStdin, include_elf, }; use sha2::{Digest, Sha256}; @@ -23,6 +23,10 @@ use clap::Parser; /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -32,7 +36,14 @@ pub struct Args { #[arg(long, env, default_value = "http://127.0.0.1:3002")] pub esplora_url: String, - #[arg(long, env)] + // Print-only mode skips runtime inputs but keeps them required otherwise. + #[arg( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub commit_info: String, #[arg(long, default_value = "commits.bin")] @@ -82,14 +93,14 @@ pub async fn fetch_commit_chain( commits_file: &str, network: Network, ) -> anyhow::Result> { - let btc_client = BTCClient::new(network, Some(&esplora_url)); + let btc_client = BTCClient::new(network, Some(esplora_url)); tracing::info!( "Fetching commit chain, commit_info_file: {}, commits_file: {}", commit_info_file, commits_file ); - let rdr = std::fs::File::open(commit_info_file).context(&("read error"))?; + let rdr = std::fs::File::open(commit_info_file).context("read error")?; let ci: CommitInfo = serde_json::from_reader(rdr)?; let mut commits: Vec = vec![]; let txid = Txid::from_str(&ci.txid)?; @@ -100,12 +111,20 @@ pub async fn fetch_commit_chain( }; let commit_txn = btc_client.get_tx(&txid).await?.unwrap(); - let op_return_data = extract_op_return_data(&commit_txn.output); - let mut sequencer_set_hash: [u8; 32] = [0u8; 32]; - sequencer_set_hash.copy_from_slice(&op_return_data[0..32]); - - if let tendermint::Hash::Sha256(expected_hash) = sequencer_hash(&ci.sequencers) { - assert_eq!(expected_hash, sequencer_set_hash); + let commitment = + extract_commit_chain_commitment(&commit_txn.output).map_err(anyhow::Error::msg)?; + if let tendermint::Hash::Sha256(sequencer_set_hash) = sequencer_hash(&ci.sequencers) { + anyhow::ensure!( + commitment + == commit_chain_commitment_digest( + sequencer_set_hash, + ci.genesis_evm_block_hash, + ci.program_history_root, + ci.proof_checkpoint_root, + ci.authorized_program_ids, + ), + "commit transaction digest does not match commit info" + ); } else { panic!("Invalid sequencer set hash"); } @@ -129,11 +148,15 @@ pub async fn fetch_commit_chain( next_publisher_public_keys, next_threshold: ci.next_threshold, genesis_txid: Txid::from_str(&ci.genesis_txid)?.as_raw_hash().to_byte_array(), + genesis_evm_block_hash: ci.genesis_evm_block_hash, + program_history_root: ci.program_history_root, block_height, + proof_checkpoint_root: ci.proof_checkpoint_root, + authorized_program_ids: ci.authorized_program_ids, }; commits.push(commit); - std::fs::write(&commits_file, serde_json::to_vec(&commits)?) - .expect(&format!("write {commits_file} error")); + std::fs::write(commits_file, serde_json::to_vec(&commits)?) + .with_context(|| format!("write {commits_file} error"))?; Ok(commits) } @@ -145,6 +168,7 @@ pub struct CommitChainProofBuilder { } impl CommitChainProofBuilder { + #[allow(clippy::new_without_default)] pub fn new() -> Self { let client = ProverClient::new(); let (proving_key, verifying_key) = client.setup(COMMIT_CHAIN); @@ -180,22 +204,23 @@ impl ProofBuilder for CommitChainProofBuilder { }; //let mut zkm_vk_hash = self.verifying_key.hash_u32(); - // Set the previous proof type based on input_proof argument + // Genesis replay deliberately skips every predecessor proof sidecar. let prev_receipt = if *init_input { None } else { - let public_inputs = fs::read(&format!("{}.public_inputs.bin", input_proof)) + let public_inputs = fs::read(format!("{}.public_inputs.bin", input_proof)) .context("Read public input")?; //let prev: CommitChainCircuitOutput = serde_json::from_slice(&public_inputs).unwrap(); Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { let proof_bytes = fs::read(input_proof).context("Failed to read input proof file")?; - let zkm_vk_hash = fs::read(&format!("{}.vk_hash.bin", input_proof)) - .context("Read vk hash")?; + let zkm_vk_hash = + fs::read(format!("{}.vk_hash.bin", input_proof)).context("Read vk hash")?; let version_path = format!("{input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| { @@ -206,22 +231,16 @@ impl ProofBuilder for CommitChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - let prev_output: CommitChainCircuitOutput = - zkm_sdk::ZKMPublicValues::from(&public_inputs).read(); - ( - CommitChainPrevProofType::PrevProof(prev_output), - proof_bytes, - public_inputs, - zkm_vk_hash.to_vec(), - zkm_version, - ) + let prev_proof = + classify_commit_chain_output(&public_inputs).map_err(anyhow::Error::msg)?; + (prev_proof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), zkm_version) } None => ( CommitChainPrevProofType::GenesisBlock, Vec::new(), Vec::new(), Vec::new(), - "v1.2.5".into(), + ZKM_CIRCUIT_VERSION.into(), ), }; @@ -230,6 +249,7 @@ impl ProofBuilder for CommitChainProofBuilder { zkm_version, zkm_proof, prev_proof, + self_program_id, commits: commits.to_vec(), zkm_public_values, }; @@ -265,10 +285,9 @@ impl ProofBuilder for CommitChainProofBuilder { tracing::info!("Commit chain proof cycles: {}", cycles); - // todo: verify the proof laterr - // if let Err(e) = self.client.verify(&proof, &self.verifying_key) { - // panic!("{}", e); - // } + self.client + .verify(&proof, &self.verifying_key) + .context("Failed to verify generated commit chain proof")?; let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) @@ -281,7 +300,7 @@ impl ProofBuilder for CommitChainProofBuilder { _cycles: u64, proof: ZKMProofWithPublicValues, ) -> anyhow::Result<(String, usize)> { - let ProofRequest::CommitChainProofRequest { output_proof, .. } = ctx else { + let ProofRequest::CommitChainProofRequest { output_proof, commits, .. } = ctx else { anyhow::bail!("Invalid commit chain input"); }; //fs::write(&output_proof, bincode::serialize(&proof)?)?; @@ -292,16 +311,19 @@ impl ProofBuilder for CommitChainProofBuilder { //tracing::info!("Generate proof successfully, proof: {:?}", proof); //Ok((public_value_hex, proof_size)) - std::fs::write(&format!("{}", output_proof), proof.bytes())?; - let public_value_hex = hex::encode(proof.public_values.to_vec()); + let public_values = proof.public_values.to_vec(); + + std::fs::write(output_proof, proof.bytes())?; + let public_value_hex = hex::encode(&public_values); let proof_size = proof.bytes().len(); let zkm_version = proof.zkm_version.clone(); - std::fs::write( - &format!("{}.public_inputs.bin", output_proof), - proof.public_values.to_vec(), - )?; - std::fs::write(&format!("{}.vk_hash.bin", output_proof), self.verifying_key.bytes32())?; - std::fs::write(&format!("{}.zkm_version.bin", output_proof), zkm_version)?; + std::fs::write(format!("{}.public_inputs.bin", output_proof), public_values)?; + std::fs::write(format!("{}.vk_hash.bin", output_proof), self.verifying_key.bytes32())?; + std::fs::write(format!("{}.zkm_version.bin", output_proof), zkm_version)?; + let latest_commit = commits.last().context("commit proof input must be non-empty")?; + let commits_path = format!("{output_proof}.commits"); + std::fs::write(&commits_path, serde_json::to_vec(std::slice::from_ref(latest_commit))?) + .with_context(|| format!("write {commits_path} error"))?; Ok((public_value_hex, proof_size)) } } @@ -317,13 +339,12 @@ mod tests { fn test_parse_commit_chain_proof() { let proof_path = "/home/ubuntu/data/proof-builder-rpc/circuits/data/commit-chain/10-1.bin.public_inputs.bin"; let proof_bytes = std::fs::read(proof_path).unwrap(); - let mut pis = zkm_sdk::ZKMPublicValues::from(&proof_bytes); - let public_input: CommitChainCircuitOutput = pis.read(); + let public_input = decode_commit_chain_circuit_output(&proof_bytes); let hash = sequencer_hash(&public_input.chain_state.sequencers); println!("proof: {public_input:?}, hash : {:?}", hash); //let input_path = "/home/ubuntu/data/proof-builder-rpc/circuits/data/commit-chain/commit_info.json.8"; - let input_path = "/home/ubuntu/data/stephen/bitvm2-node/circuits/data/commit-chain/commit_info.json.latest"; + let input_path = "/home/ubuntu/data/stephen/bitvm-node/circuits/data/commit-chain/commit_info.json.latest"; let input_bytes = std::fs::read(input_path).unwrap(); let commit_info: CommitInfo = serde_json::from_slice(&input_bytes).unwrap(); info!("commit info: {:?}", commit_info); diff --git a/circuits/commit-chain-proof/host/src/main.rs b/circuits/commit-chain-proof/host/src/main.rs index c6fd0344b..ab72bbe96 100644 --- a/circuits/commit-chain-proof/host/src/main.rs +++ b/circuits/commit-chain-proof/host/src/main.rs @@ -11,6 +11,12 @@ async fn main() { zkm_sdk::utils::setup_logger(); tracing::info!("args: {:?}", args); + let builder = CommitChainProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let commits = fetch_commit_chain( &args.esplora_url, &args.commit_info, @@ -19,8 +25,6 @@ async fn main() { ) .await .unwrap(); - let builder = CommitChainProofBuilder::new(); - let ctx = ProofRequest::CommitChainProofRequest { init_input: args.init_input, input_proof: args.input_proof.clone(), diff --git a/circuits/cron-header-chain-proof.sh b/circuits/cron-header-chain-proof.sh index 5307187a2..f41bdf4c7 100644 --- a/circuits/cron-header-chain-proof.sh +++ b/circuits/cron-header-chain-proof.sh @@ -12,15 +12,32 @@ batch=${2:-$_batch} function find_input_proof() { local start="$1" - local input_file - input_file=$(find $DATA -maxdepth 1 -type f -regex '.*[0-9]+-[0-9]+\.bin$' -printf '%f\n' | - awk -v sum="$start" -F '[-.]' '($1 + $2) == sum { print $0; exit }') + local input_file="" + local proof_path + local proof_file + local proof_start + local proof_batch - if [ ! $input_file ]; then - echo "Can not find the input proof" - exit -1 + # Match proof files by filename because batch size may vary between runs. + shopt -s nullglob + for proof_path in "$DATA"/*.bin; do + proof_file="${proof_path##*/}" + if [[ "$proof_file" =~ ^([0-9]+)-([0-9]+)\.bin$ ]]; then + proof_start="${BASH_REMATCH[1]}" + proof_batch="${BASH_REMATCH[2]}" + if (( proof_start + proof_batch == start )); then + input_file="$proof_file" + break + fi + fi + done + shopt -u nullglob + + if [ -z "$input_file" ]; then + echo "Can not find the input proof for start=$start in $DATA" >&2 + exit 1 fi - echo $input_file + echo "$input_file" } if [ $start -ne 0 ]; then diff --git a/circuits/cron-state-chain-proof.sh b/circuits/cron-state-chain-proof.sh index 5e94224f7..b3e561bf2 100644 --- a/circuits/cron-state-chain-proof.sh +++ b/circuits/cron-state-chain-proof.sh @@ -9,15 +9,32 @@ batch=${2:-$_batch} function find_input_proof() { local start="$1" - local input_file - input_file=$(find $DATA -maxdepth 1 -type f -name '*-*.bin' -printf '%f\n' | - awk -v sum="$start" -F '[-.]' '($1 + $2) == sum { print $0; exit }') + local input_file="" + local proof_path + local proof_file + local proof_start + local proof_batch - if [ ! $input_file ]; then - echo "Can not find the input proof" - exit -1 + # Match proof files by filename because batch size may vary between runs. + shopt -s nullglob + for proof_path in "$DATA"/*.bin; do + proof_file="${proof_path##*/}" + if [[ "$proof_file" =~ ^([0-9]+)-([0-9]+)\.bin$ ]]; then + proof_start="${BASH_REMATCH[1]}" + proof_batch="${BASH_REMATCH[2]}" + if (( proof_start + proof_batch == start )); then + input_file="$proof_file" + break + fi + fi + done + shopt -u nullglob + + if [ -z "$input_file" ]; then + echo "Can not find the input proof for start=$start in $DATA" >&2 + exit 1 fi - echo $input_file + echo "$input_file" } if [ $start -ne $EL_START_BLOCK_NUMBER ]; then diff --git a/circuits/data/watchtower/output3.bin.proof_part_stark_vk.bin b/circuits/data/watchtower/output3.bin.proof_part_stark_vk.bin deleted file mode 100644 index 7fb52b0c0..000000000 Binary files a/circuits/data/watchtower/output3.bin.proof_part_stark_vk.bin and /dev/null differ diff --git a/circuits/header-chain-proof/guest/Cargo.lock b/circuits/header-chain-proof/guest/Cargo.lock index 6e9bc5a88..f72deaeb0 100644 --- a/circuits/header-chain-proof/guest/Cargo.lock +++ b/circuits/header-chain-proof/guest/Cargo.lock @@ -347,6 +347,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -512,6 +526,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -572,7 +595,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version", @@ -1011,7 +1034,6 @@ dependencies = [ "log", "sha2", "tracing", - "zkm-verifier", "zkm-zkvm", ] @@ -1110,14 +1132,16 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2", - "zkm-verifier", + "verifier", + "zkm-primitives 1.2.7", "zkm-zkvm", ] @@ -1300,7 +1324,7 @@ dependencies = [ "once_cell", "sha2", "signature", - "zkm-lib", + "zkm-lib 1.2.5", ] [[package]] @@ -1309,7 +1333,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -1589,7 +1613,7 @@ dependencies = [ "hex", "primeorder", "sha2", - "zkm-lib", + "zkm-lib 1.2.5", ] [[package]] @@ -2360,7 +2384,7 @@ version = "0.10.9" source = "git+https://github.com/ziren-patches/RustCrypto-hashes?branch=patch-sha2-0.10.9#dbfdbd088ac7b55e03c0b9eb192278efe35d67dc" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest", ] @@ -2484,7 +2508,7 @@ dependencies = [ "num-bigint 0.4.6", "rand", "rustc-hex", - "zkm-lib", + "zkm-lib 1.2.5", ] [[package]] @@ -2770,6 +2794,14 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "sha2", + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" @@ -3184,8 +3216,8 @@ dependencies = [ [[package]] name = "zkm-core-executor" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -3219,14 +3251,14 @@ dependencies = [ "typenum", "vec_map", "zkm-curves", - "zkm-primitives", + "zkm-primitives 1.2.7", "zkm-stark", ] [[package]] name = "zkm-core-machine" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -3271,14 +3303,14 @@ dependencies = [ "zkm-core-executor", "zkm-curves", "zkm-derive", - "zkm-primitives", + "zkm-primitives 1.2.7", "zkm-stark", ] [[package]] name = "zkm-curves" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "cfg-if", "curve25519-dalek", @@ -3295,14 +3327,14 @@ dependencies = [ "thiserror 1.0.69", "tracing", "typenum", - "zkm-primitives", + "zkm-primitives 1.2.7", "zkm-stark", ] [[package]] name = "zkm-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "proc-macro2", "quote", @@ -3319,7 +3351,20 @@ dependencies = [ "elliptic-curve", "serde", "sha2", - "zkm-primitives", + "zkm-primitives 1.2.5", +] + +[[package]] +name = "zkm-lib" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" +dependencies = [ + "bincode", + "cfg-if", + "elliptic-curve", + "serde", + "sha2", + "zkm-primitives 1.2.7", ] [[package]] @@ -3340,10 +3385,30 @@ dependencies = [ "sha2", ] +[[package]] +name = "zkm-primitives" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" +dependencies = [ + "bincode", + "blake3", + "hex", + "lazy_static", + "num-bigint 0.4.6", + "p3-field", + "p3-koala-bear", + "p3-monty-31", + "p3-poseidon2", + "p3-symmetric", + "serde", + "sha2", + "tracing", +] + [[package]] name = "zkm-recursion-core" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "backtrace", "ff 0.13.1", @@ -3373,14 +3438,14 @@ dependencies = [ "zkhash", "zkm-core-machine", "zkm-derive", - "zkm-primitives", + "zkm-primitives 1.2.7", "zkm-stark", ] [[package]] name = "zkm-stark" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -3415,14 +3480,14 @@ dependencies = [ "tracing-forest", "tracing-subscriber", "zkm-derive", - "zkm-primitives", + "zkm-primitives 1.2.7", "zkm-zkvm", ] [[package]] name = "zkm-verifier" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -3444,17 +3509,18 @@ dependencies = [ "thiserror 2.0.18", "zkm-core-executor", "zkm-core-machine", - "zkm-primitives", + "zkm-primitives 1.2.7", "zkm-recursion-core", "zkm-stark", ] [[package]] name = "zkm-zkvm" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "cfg-if", "getrandom 0.2.17", "lazy_static", @@ -3464,8 +3530,8 @@ dependencies = [ "rand", "serde", "sha2", - "zkm-lib", - "zkm-primitives", + "zkm-lib 1.2.7", + "zkm-primitives 1.2.7", ] [[package]] diff --git a/circuits/header-chain-proof/guest/Cargo.toml b/circuits/header-chain-proof/guest/Cargo.toml index 42e819c23..bc6e86122 100644 --- a/circuits/header-chain-proof/guest/Cargo.toml +++ b/circuits/header-chain-proof/guest/Cargo.toml @@ -13,9 +13,7 @@ header-chain = { path = "../../../crates/header-chain" } borsh = { version = "1.5.3", features = ["derive"] } # Ziren -zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } -zkm-verifier = { git = "https://github.com/ProjectZKM/Ziren" } -#zkm-verifier = { path = "../../../Ziren/crates/verifier" } +zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7", features = ["verify"] } #zkm-zkvm = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging @@ -38,3 +36,6 @@ alloy-primitives-v1-1-0 = { git = "https://github.com/ziren-patches/core.git", p alloy-primitives-v1-1-2 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.1.2" } secp256k1-v0-29-1 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.29.1" } #secp256k1-v0-30-0 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.30.0" } + +[features] +imm-wrap-vk = ["zkm-zkvm/imm-wrap-vk", "header-chain/imm-wrap-vk"] diff --git a/circuits/header-chain-proof/host/Cargo.toml b/circuits/header-chain-proof/host/Cargo.toml index 8c98b6a42..9704cc079 100644 --- a/circuits/header-chain-proof/host/Cargo.toml +++ b/circuits/header-chain-proof/host/Cargo.toml @@ -23,12 +23,13 @@ sha2 = { workspace = true } rand = { workspace = true } borsh = { workspace = true } -# bitvm2 node +# bitvm node header-chain = { workspace = true } bitcoin = { workspace = true } client = { workspace = true } proof-builder = { workspace = true } util = { workspace = true } +verifier = { workspace = true } # Ziren zkm-sdk.workspace = true diff --git a/circuits/header-chain-proof/host/src/lib.rs b/circuits/header-chain-proof/host/src/lib.rs index 9710a5d6b..7801b90eb 100644 --- a/circuits/header-chain-proof/host/src/lib.rs +++ b/circuits/header-chain-proof/host/src/lib.rs @@ -1,7 +1,10 @@ use bitcoin::Network; use borsh::{BorshDeserialize, BorshSerialize}; use client::btc_chain::BTCClient; -use header_chain::{CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType}; +use header_chain::{ + CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType, + classify_header_chain_output, +}; use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; use sha2::{Digest, Sha256}; use std::{ @@ -10,7 +13,10 @@ use std::{ }; use util::get_btc_block_confirms; use zkm_sdk::ZKMProofKind; -use zkm_sdk::{HashableKey, Prover, ProverClient, ZKMProofWithPublicValues, ZKMStdin, include_elf}; +use zkm_sdk::{ + HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofWithPublicValues, ZKMStdin, + include_elf, +}; static ELF_ID: OnceLock = OnceLock::new(); use anyhow::Context; use clap::Parser; @@ -19,6 +25,10 @@ use std::sync::OnceLock; /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -80,8 +90,9 @@ pub async fn fetch_header_chain( .read(true) .write(true) .create(true) + .truncate(false) .open(block_header_file) - .expect(&format!("Open {block_header_file} error")); + .with_context(|| format!("Open {block_header_file} error"))?; let mut headers: Vec = Vec::new(); writer.read_to_end(&mut headers)?; @@ -151,6 +162,7 @@ pub struct HeaderChainProofBuilder { } impl HeaderChainProofBuilder { + #[allow(clippy::new_without_default)] pub fn new() -> Self { let client = ProverClient::new(); let (proving_key, verifying_key) = client.setup(HEADER_CHAIN); @@ -197,18 +209,20 @@ impl ProofBuilder for HeaderChainProofBuilder { let prev_receipt = if *init_input { None } else { - let public_inputs = fs::read(&format!("{}.public_inputs.bin", input_proof)).expect( - &format!("Failed to read public inputs from {}.public_inputs.bin", input_proof), - ); + let public_inputs = fs::read(format!("{}.public_inputs.bin", input_proof)) + .with_context(|| { + format!("Failed to read public inputs from {}.public_inputs.bin", input_proof) + })?; Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { let proof_bytes = fs::read(input_proof).context("Failed to read input proof file").unwrap(); - let zkm_vk_hash = fs::read(&format!("{}.vk_hash.bin", input_proof)).unwrap(); + let zkm_vk_hash = fs::read(format!("{}.vk_hash.bin", input_proof)).unwrap(); let version_path = format!("{input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| { @@ -219,21 +233,16 @@ impl ProofBuilder for HeaderChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - let prev_output = zkm_sdk::ZKMPublicValues::from(&public_inputs).read(); - ( - HeaderChainPrevProofType::PrevProof(prev_output), - proof_bytes, - public_inputs, - zkm_vk_hash.to_vec(), - zkm_version, - ) + let prev_proof = + classify_header_chain_output(&public_inputs).map_err(anyhow::Error::msg)?; + (prev_proof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), zkm_version) } None => ( HeaderChainPrevProofType::GenesisBlock, Vec::new(), Vec::new(), Vec::new(), - "v1.2.5".into(), + ZKM_CIRCUIT_VERSION.into(), ), }; @@ -244,13 +253,14 @@ impl ProofBuilder for HeaderChainProofBuilder { batch_size ); - let block_headers = (&total_block_headers[*start..*start + *batch_size]).to_vec(); + let block_headers = total_block_headers[*start..*start + *batch_size].to_vec(); let input: HeaderChainCircuitInput = HeaderChainCircuitInput { prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers, }; @@ -282,9 +292,9 @@ impl ProofBuilder for HeaderChainProofBuilder { tracing::info!("Header chain proof cycles: {}", cycles); - if let Err(e) = self.client.verify(&proof, &self.verifying_key) { - panic!("{}", e); - } + self.client + .verify(&proof, &self.verifying_key) + .context("Failed to verify generated header chain proof")?; let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) @@ -306,16 +316,16 @@ impl ProofBuilder for HeaderChainProofBuilder { //fs::write(&format!("{}.vk", output_proof), bincode::serialize(&self.verifying_key)?)?; //fs::write(&format!("{}.in", output_proof), input)?; - std::fs::write(&format!("{}", output_proof), proof.bytes())?; + std::fs::write(output_proof, proof.bytes())?; let public_value_hex = hex::encode(proof.public_values.to_vec()); let proof_size = proof.bytes().len(); let zkm_version = proof.zkm_version.clone(); std::fs::write( - &format!("{}.public_inputs.bin", output_proof), + format!("{}.public_inputs.bin", output_proof), proof.public_values.to_vec(), )?; - std::fs::write(&format!("{}.vk_hash.bin", output_proof), self.verifying_key.bytes32())?; - std::fs::write(&format!("{}.zkm_version.bin", output_proof), zkm_version)?; + std::fs::write(format!("{}.vk_hash.bin", output_proof), self.verifying_key.bytes32())?; + std::fs::write(format!("{}.zkm_version.bin", output_proof), zkm_version)?; tracing::info!("Generate proof successfully, proof: {:?}", proof); Ok((public_value_hex, proof_size)) diff --git a/circuits/header-chain-proof/host/src/main.rs b/circuits/header-chain-proof/host/src/main.rs index 0c4db794a..b128457f9 100644 --- a/circuits/header-chain-proof/host/src/main.rs +++ b/circuits/header-chain-proof/host/src/main.rs @@ -12,6 +12,12 @@ async fn main() { zkm_sdk::utils::setup_logger(); tracing::info!("args: {args:?}"); + let builder = HeaderChainProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let total_block_headers = fetch_header_chain( &args.esplora_url, args.start, @@ -23,8 +29,6 @@ async fn main() { .await .unwrap(); - let builder = HeaderChainProofBuilder::new(); - let ctx = ProofRequest::HeaderChainProofRequest { init_input: args.init_input, input_proof: args.input_proof.clone(), diff --git a/circuits/operator-proof/guest/Cargo.lock b/circuits/operator-proof/guest/Cargo.lock index 2a543d93e..2db629606 100644 --- a/circuits/operator-proof/guest/Cargo.lock +++ b/circuits/operator-proof/guest/Cargo.lock @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "alloy-primitives" version = "1.4.1" -source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#59313700f710f1373a5b4cdc05c51dc3227d8064" +source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#0d034202c9977acd601399efdd9c30fd1c1a08a0" dependencies = [ "alloy-rlp", "bytes", @@ -1008,7 +1008,7 @@ checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-light-client-circuit" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1022,11 +1022,11 @@ dependencies = [ "revm-database-interface", "serde", "serde_json", - "sha2 0.10.9", "state-chain", "tendermint", "tendermint-light-client-verifier", "tracing", + "verifier", "zkm-primitives", "zkm-verifier", "zkm-zkvm", @@ -1095,6 +1095,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1129,7 +1143,7 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#bcf9397426410acc171ad625416b508dc913be8e" +source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#97b215f80e9bfab9ad6641e41b005b6d8e58f748" dependencies = [ "cfg-if", "ff 0.13.1", @@ -1282,7 +1296,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commit-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1297,7 +1311,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -1308,7 +1322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -1381,6 +1395,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -1462,7 +1485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version 0.4.1", @@ -2276,14 +2299,13 @@ dependencies = [ "sha2 0.10.9", "state-chain", "tracing", - "zkm-verifier", "zkm-zkvm", ] [[package]] name = "guest-executor" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-consensus", "alloy-evm", @@ -2427,14 +2449,16 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -2800,7 +2824,7 @@ dependencies = [ [[package]] name = "k256" version = "0.13.4" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#8266b228a39402a0ba68d644b7f26b85b5112fe3" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#6ad84b9b604911c6f5dd353b88c04927d2739a32" dependencies = [ "cfg-if", "ecdsa", @@ -2819,7 +2843,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -2964,7 +2988,7 @@ dependencies = [ [[package]] name = "mpt" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -3221,7 +3245,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p256" version = "0.13.2" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "ecdsa", "elliptic-curve", @@ -3780,7 +3804,7 @@ dependencies = [ [[package]] name = "primeorder" version = "0.13.1" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "elliptic-curve", ] @@ -3799,7 +3823,7 @@ dependencies = [ [[package]] name = "primitives" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-eips", "alloy-genesis", @@ -5050,7 +5074,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -5061,7 +5085,7 @@ version = "0.10.9" source = "git+https://github.com/ziren-patches/RustCrypto-hashes?branch=patch-sha2-0.10.9#dbfdbd088ac7b55e03c0b9eb192278efe35d67dc" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5165,7 +5189,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -5186,7 +5210,8 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5254,7 +5279,7 @@ dependencies = [ [[package]] name = "substrate-bn" version = "0.6.0" -source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" +source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#0dfeeb1b7bfc21941b6e4964d678abd094a54a6f" dependencies = [ "bytemuck", "byteorder", @@ -5755,6 +5780,14 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "sha2 0.10.9", + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" @@ -6323,8 +6356,8 @@ dependencies = [ [[package]] name = "zkm-core-executor" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6364,8 +6397,8 @@ dependencies = [ [[package]] name = "zkm-core-machine" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6416,8 +6449,8 @@ dependencies = [ [[package]] name = "zkm-curves" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "cfg-if", "curve25519-dalek", @@ -6440,8 +6473,8 @@ dependencies = [ [[package]] name = "zkm-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "proc-macro2", "quote", @@ -6450,8 +6483,8 @@ dependencies = [ [[package]] name = "zkm-lib" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6463,10 +6496,11 @@ dependencies = [ [[package]] name = "zkm-primitives" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -6477,12 +6511,13 @@ dependencies = [ "p3-symmetric", "serde", "sha2 0.10.9", + "tracing", ] [[package]] name = "zkm-recursion-core" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "backtrace", "ff 0.13.1", @@ -6518,8 +6553,8 @@ dependencies = [ [[package]] name = "zkm-stark" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -6560,8 +6595,8 @@ dependencies = [ [[package]] name = "zkm-verifier" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6590,10 +6625,11 @@ dependencies = [ [[package]] name = "zkm-zkvm" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "cfg-if", "getrandom 0.2.17", "lazy_static", diff --git a/circuits/operator-proof/guest/Cargo.toml b/circuits/operator-proof/guest/Cargo.toml index 521f3c951..2defaac0a 100644 --- a/circuits/operator-proof/guest/Cargo.toml +++ b/circuits/operator-proof/guest/Cargo.toml @@ -14,9 +14,7 @@ state-chain = { path = "../../../crates/state-chain" } bitcoin-light-client-circuit = { path = "../../../crates/bitcoin-light-client-circuit" } # Ziren -zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } -zkm-verifier = { git = "https://github.com/ProjectZKM/Ziren" } -#zkm-verifier = { path = "../../../Ziren/crates/verifier" } +zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7", features = ["verify"] } #zkm-zkvm = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging @@ -28,7 +26,6 @@ alloy-primitives = { version = "1.0.0", features = ["sha3-keccak", "map-foldhash #revm = { git = "https://github.com/ziren-patches/revm", branch = "patch-31.0.2", features = ["serde", "bn"], default-features = false } sha2 = "0.10.9" - [patch.crates-io] # Precompile patches sha2 = { git = "https://github.com/ziren-patches/RustCrypto-hashes", branch = "patch-sha2-0.10.9", package = "sha2" } @@ -43,3 +40,8 @@ secp256k1-v0-29-1 = { git = "https://github.com/ziren-patches/rust-secp256k1", p #secp256k1-v0-30-0 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.30.0" } #bitcoin = { git = "https://github.com/ProjectZKM/rust-bitcoin", branch = "patch-0.32.7" } +[features] +imm-wrap-vk = [ + "zkm-zkvm/imm-wrap-vk", + "bitcoin-light-client-circuit/imm-wrap-vk", +] diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index f5c6dbcdc..51e7cfd52 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -1,51 +1,47 @@ #![no_main] zkm_zkvm::entrypoint!(main); -use std::str::FromStr; -use header_chain::{ - HeaderChainCircuitInput, SPV, -}; -use alloy_primitives::{U256, Address}; -use bitcoin_light_client_circuit::{EthClientExecutorInput, OperatorAttestationInputs}; +use alloy_primitives::U256; +use bitcoin::Transaction; +use bitcoin_light_client_circuit::IndexedWatchtowerChallenge; use commit_chain::CommitChainCircuitInput; +use header_chain::{HeaderChainCircuitInput, SPV}; use state_chain::StateChainCircuitInput; -use bitcoin::{ScriptBuf, TxOut, Transaction}; pub fn main() { // calculate operator public input: https://github.com/ProjectZKM/Ziren/blob/main/crates/sdk/src/utils.rs#L42 - let included_watchertowers: U256 = zkm_zkvm::io::read::(); + let included_watchtowers: U256 = zkm_zkvm::io::read::(); let graph_id: [u8; 16] = zkm_zkvm::io::read::<[u8; 16]>(); - let operator_genesis_sequencer_commit_txid: [u8; 32] = zkm_zkvm::io::read(); - println!("read operator commit txn"); - let operator_latest_sequencer_commit_txn: Transaction = zkm_zkvm::io::read(); // private inputs - let latest_sequencer_commit_txid = operator_latest_sequencer_commit_txn.compute_txid(); // public input + let operator_genesis_sequencer_commit_txid: [u8; 32] = zkm_zkvm::io::read(); + // https://github.com/KSlashh/BitVM/blob/v2/goat/src/transactions/watchtower_challenge.rs#L128 - let watchtower_challenge_txns: Vec = zkm_zkvm::io::read(); - let watchtower_challenge_txn_pubkey: Vec = zkm_zkvm::io::read(); - let watchtower_challenge_txn_scripts: Vec = zkm_zkvm::io::read(); - let watchtower_challenge_txn_prev_outs: Vec = zkm_zkvm::io::read(); + let watchtower_challenge_init_txid: [u8; 32] = zkm_zkvm::io::read(); + let watchtower_challenge_init_txn: Option = zkm_zkvm::io::read(); + let graph_watchtower_xonly_public_keys: Vec<[u8; 32]> = zkm_zkvm::io::read(); + let watchtower_challenges: Vec = zkm_zkvm::io::read(); let operator_header_chain: HeaderChainCircuitInput = zkm_zkvm::io::read(); let operator_commit_chain: CommitChainCircuitInput = zkm_zkvm::io::read(); let operator_state_chain: StateChainCircuitInput = zkm_zkvm::io::read(); - let attestation: OperatorAttestationInputs = zkm_zkvm::io::read(); let spv_ss_commit: SPV = zkm_zkvm::io::read(); let operator_committed_blockhash: [u8; 32] = zkm_zkvm::io::read(); - let output = bitcoin_light_client_circuit::propose_longest_chain( - included_watchertowers, - graph_id, - operator_genesis_sequencer_commit_txid, - watchtower_challenge_txns, - watchtower_challenge_txn_pubkey, - watchtower_challenge_txn_scripts, - watchtower_challenge_txn_prev_outs, - operator_header_chain, - operator_commit_chain, - operator_state_chain, - attestation, - spv_ss_commit, - operator_committed_blockhash, - ); + let (btc_best_block_hash, constant, included_watchtowers) = + bitcoin_light_client_circuit::propose_longest_chain( + included_watchtowers, + graph_id, + operator_genesis_sequencer_commit_txid, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenges, + &graph_watchtower_xonly_public_keys, + operator_header_chain, + operator_commit_chain, + operator_state_chain, + spv_ss_commit, + operator_committed_blockhash, + ); - zkm_zkvm::io::commit(&output); + zkm_zkvm::io::commit(&btc_best_block_hash); + zkm_zkvm::io::commit(&constant); + zkm_zkvm::io::commit(&included_watchtowers); } diff --git a/circuits/operator-proof/host/Cargo.toml b/circuits/operator-proof/host/Cargo.toml index 2bc6c649f..7b22071a1 100644 --- a/circuits/operator-proof/host/Cargo.toml +++ b/circuits/operator-proof/host/Cargo.toml @@ -43,6 +43,7 @@ cbft-rpc = { workspace = true } bitcoin = { workspace = true } client = { workspace = true } proof-builder.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true @@ -50,7 +51,7 @@ zkm-prover.workspace = true zkm-verifier = { workspace = true, features = ["ark"] } [dev-dependencies] -bitvm2-lib.workspace = true +bitvm-lib.workspace = true [build-dependencies] zkm-build.workspace = true diff --git a/circuits/operator-proof/host/build.rs b/circuits/operator-proof/host/build.rs index 0c21839a1..dcfa0c539 100644 --- a/circuits/operator-proof/host/build.rs +++ b/circuits/operator-proof/host/build.rs @@ -1,4 +1,5 @@ use zkm_build::build_program; + fn main() { build_program("../guest"); } diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 4d2f0fc0a..eca6e2195 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -1,40 +1,29 @@ //! Generate operator proof use alloy_primitives::U256; use anyhow::Context; -use bitcoin::{ - BlockHash, Network, ScriptBuf, Transaction, TxOut, Txid, - hashes::Hash, - secp256k1::{PublicKey, XOnlyPublicKey}, -}; +use bitcoin::{Block, BlockHash, Network, Transaction, Txid, hashes::Hash, secp256k1::PublicKey}; +use bitcoin_light_client_circuit::{IndexedWatchtowerChallenge, build_spv}; use borsh::BorshDeserialize; +use clap::Parser; use client::btc_chain::BTCClient; -use commit_chain::{ - CommitChainCircuitInput, CommitChainPrevProofType, extract_data_from_commitment_outputs, -}; -use header_chain::{ - BlockHeaderCircuitOutput, CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType, -}; +use commit_chain::{CommitChainCircuitInput, CommitChainPrevProofType}; +use header_chain::{CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType}; use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; -use state_chain::{StateChainCircuitInput, StateChainCircuitOutput, StateChainPrevProofType}; +use state_chain::{StateChainCircuitInput, StateChainPrevProofType}; use std::str::FromStr; use util::get_btc_block_confirms; use zkm_sdk::{ HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, include_elf, }; -use zkm_verifier::Groth16Verifier; - -use bincode::deserialize; -use bitcoin_light_client_circuit::{ - OperatorAttestationInputs, build_spv, load_unique_part_stark_vk_witnesses, - parse_watchtower_commitment, part_stark_vk_attestation_dir, -}; -use bitcoin_script::script; -use clap::Parser; /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -44,40 +33,119 @@ pub struct Args { #[arg(long, env, default_value_t = Network::Regtest)] pub bitcoin_network: Network, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub included_watchtowers: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub graph_id: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub latest_sequencer_commit_txid: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub operator_committed_blockhash: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub genesis_sequencer_commit_txid: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'H', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub header_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'c', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub commit_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 's', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub state_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'e', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("0")) + )] pub execution_layer_block_number: u64, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 't', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_challenge_txids: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'w', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_public_keys: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'i', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_challenge_init_txid: String, #[clap(long, env, default_value = "commit-proof.bin")] @@ -93,14 +161,47 @@ impl LongRunning for Args { /// A program that aggregates the proofs of the simple program. const OPERATOR: &[u8] = include_elf!("guest"); -use serde::de::DeserializeOwned; use std::fs; -use std::panic::{AssertUnwindSafe, catch_unwind}; use sha2::{Digest, Sha256}; use std::sync::OnceLock; static ELF_ID: OnceLock = OnceLock::new(); +type IndexedWatchtowerInputs = Vec<(u16, Txid)>; +type GraphWatchtowerXOnlyPublicKeys = Vec<[u8; 32]>; + +/// Parses the full graph key list and keeps each included challenge's original graph index. +fn parse_indexed_watchtower_inputs( + watchtower_challenge_txids: &str, + watchtower_public_keys: &str, +) -> anyhow::Result<(IndexedWatchtowerInputs, GraphWatchtowerXOnlyPublicKeys)> { + let public_keys = watchtower_public_keys + .split(',') + .map(PublicKey::from_str) + .collect::, _>>()?; + let txids = if watchtower_challenge_txids.trim().is_empty() { + vec![""; public_keys.len()] + } else { + watchtower_challenge_txids.split(',').collect::>() + }; + anyhow::ensure!( + txids.len() == public_keys.len(), + "watchtower challenge txids and public keys must have equal lengths" + ); + anyhow::ensure!(!public_keys.is_empty(), "watchtower public key list must not be empty"); + anyhow::ensure!(public_keys.len() <= 256, "watchtower public key list exceeds 256 entries"); + + let graph_keys = public_keys.iter().map(|key| key.x_only_public_key().0.serialize()).collect(); + let included = txids + .iter() + .enumerate() + .filter(|(_, txid)| !txid.trim().is_empty()) + .map(|(index, txid)| Ok((index as u16, Txid::from_str(txid)?))) + .collect::>>()?; + + Ok((included, graph_keys)) +} + pub async fn fetch_target_block_and_watchtower_tx( esplora_url: &str, latest_sequencer_commit_txid: &str, @@ -114,16 +215,17 @@ pub async fn fetch_target_block_and_watchtower_tx( bitcoin::Block, BlockHash, Transaction, - Vec, - Vec, - Vec, - Vec, + Vec<[u8; 32]>, + Txid, + Option, + Vec<(u16, u32, Block, Transaction)>, )> { - let watchtower_challenge_txids: Vec<&str> = watchtower_challenge_txids.split(",").collect(); - let watchtower_public_keys: Vec<&str> = watchtower_public_keys.split(",").collect(); - let btc_client = BTCClient::new(bitcoin_network, Some(&esplora_url)); + let (indexed_watchtower_inputs, graph_watchtower_xonly_public_keys) = + parse_indexed_watchtower_inputs(watchtower_challenge_txids, watchtower_public_keys)?; + let watchtower_challenge_init_txid = Txid::from_str(watchtower_challenge_init_txid)?; + let btc_client = BTCClient::new(bitcoin_network, Some(esplora_url)); - let latest_sequencer_commit_txid = Txid::from_str(&latest_sequencer_commit_txid)?; + let latest_sequencer_commit_txid = Txid::from_str(latest_sequencer_commit_txid)?; let operator_latest_sequencer_commit_txn = match btc_client.get_tx(&latest_sequencer_commit_txid).await? { Some(tx) => tx, @@ -134,7 +236,7 @@ pub async fn fetch_target_block_and_watchtower_tx( }; let tx_status = btc_client.get_tx_status(&latest_sequencer_commit_txid).await?; let block_pos_ss_commit = match tx_status.block_height { - Some(height) => height as u32, + Some(height) => height, None => anyhow::bail!( "Latest sequencer commit txn is not confirmed yet: {}", latest_sequencer_commit_txid @@ -171,48 +273,40 @@ pub async fn fetch_target_block_and_watchtower_tx( } // --- watchtower_challenge_txns --- // - let mut watchtower_challenge_txns = Vec::new(); - let mut watchtower_challenge_txn_prev_outs: Vec = Vec::new(); - let mut watchtower_challenge_txn_pubkeys = Vec::new(); - let mut watchtower_challenge_txn_scripts: Vec = Vec::new(); + let mut watchtower_challenge_witnesses = Vec::new(); - let watchtower_challlenge_init_txn: Transaction = - match btc_client.get_tx(&watchtower_challenge_init_txid.parse().unwrap()).await? { + let watchtower_challenge_init_txn = if indexed_watchtower_inputs.is_empty() { + None + } else { + let transaction = match btc_client.get_tx(&watchtower_challenge_init_txid).await? { Some(tx) => tx, None => anyhow::bail!( "Failed to fetch watchtower challenge init txn: {}", watchtower_challenge_init_txid ), }; + anyhow::ensure!( + transaction.compute_txid() == watchtower_challenge_init_txid, + "Fetched watchtower challenge init transaction has the wrong txid" + ); + Some(transaction) + }; - for (id, pk) in watchtower_challenge_txids.iter().zip(watchtower_public_keys.iter()) { - tracing::info!("txid: {}, pk: {}", id, pk); - let txid = id.parse()?; + for (node_index, txid) in indexed_watchtower_inputs { + tracing::info!("watchtower challenge txid: {txid}"); let txn = match btc_client.get_tx(&txid).await? { Some(tx) => tx, - None => anyhow::bail!("Failed to fetch watchtower challenge txn: {}", id), + None => anyhow::bail!("Failed to fetch watchtower challenge txn: {}", txid), }; - // get prev outs - // FIXME: update the index - let index = txn.input[0].previous_output.vout as usize; - watchtower_challenge_txn_prev_outs - .push(watchtower_challlenge_init_txn.output[index].clone()); - - let public_key = PublicKey::from_str(pk).unwrap(); - watchtower_challenge_txn_pubkeys.push(public_key.clone()); - watchtower_challenge_txns.push(txn); - - // https://github.com/GOATNetwork/BitVM/blob/GA/goat/src/transactions/watchtower_challenge.rs#L45 - // generate_pay_to_pubkey_taproot_script - let watchtower_challenge_txn_script: ScriptBuf = { - let public_key: XOnlyPublicKey = public_key.into(); - script! { - { public_key } - OP_CHECKSIG - } - .compile() + let status = btc_client.get_tx_status(&txid).await?; + let block_height = status + .block_height + .ok_or_else(|| anyhow::anyhow!("watchtower challenge is not confirmed: {txid}"))?; + let block = btc_client.get_block_by_height(block_height).await?; + if !block.txdata.iter().any(|candidate| candidate.compute_txid() == txid) { + anyhow::bail!("watchtower challenge is missing from its reported block: {txid}"); }; - watchtower_challenge_txn_scripts.push(watchtower_challenge_txn_script); + watchtower_challenge_witnesses.push((node_index, block_height, block, txn)); } Ok(( @@ -220,10 +314,10 @@ pub async fn fetch_target_block_and_watchtower_tx( target_block_ss_commit, operator_committed_blockhash, operator_latest_sequencer_commit_txn, - watchtower_challenge_txns, - watchtower_challenge_txn_prev_outs, - watchtower_challenge_txn_pubkeys, - watchtower_challenge_txn_scripts, + graph_watchtower_xonly_public_keys, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, )) } pub struct OperatorProofBuilder { @@ -234,6 +328,7 @@ pub struct OperatorProofBuilder { } impl OperatorProofBuilder { + #[allow(clippy::new_without_default)] pub fn new() -> Self { let client = ProverClient::new(); let (proving_key, verifying_key) = client.setup(OPERATOR); @@ -241,46 +336,6 @@ impl OperatorProofBuilder { } } -fn load_proof_public_output(proof_path: &str) -> anyhow::Result { - let public_inputs = fs::read(format!("{proof_path}.public_inputs.bin")) - .context("Failed to read public inputs")?; - deserialize(&public_inputs).context("Failed to decode proof public outputs") -} - -fn extract_watchtower_part_stark_vk(tx: &Transaction) -> Option> { - let commitment = extract_data_from_commitment_outputs(&tx.output); - let (_, _, _, _, proof_part_stark_vk) = parse_watchtower_commitment(&commitment).ok()?; - Some(proof_part_stark_vk) -} - -fn load_part_stark_vk(zkm_version: &str) -> anyhow::Result> { - catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec())) - .map_err(|_| anyhow::anyhow!("Failed to load part_stark_vk for zkm_version {zkm_version}")) -} - -/// Collect both the version-derived verifier key and the recursive inner verifier key -/// for header/state subproofs, plus each watchtower proof verifier key from commitments. -fn collect_requested_part_stark_vks( - header_chain_input: &HeaderChainCircuitInput, - header_chain_output: &BlockHeaderCircuitOutput, - state_chain_input: &StateChainCircuitInput, - state_chain_output: &StateChainCircuitOutput, - watchtower_challenge_txns: &[Transaction], -) -> anyhow::Result>> { - let mut requested_part_stark_vks = vec![ - load_part_stark_vk(&header_chain_input.zkm_version)?, - header_chain_output.part_stark_vk.clone(), - load_part_stark_vk(&state_chain_input.zkm_version)?, - state_chain_output.part_stark_vk.clone(), - ]; - for tx in watchtower_challenge_txns { - if let Some(part_stark_vk) = extract_watchtower_part_stark_vk(tx) { - requested_part_stark_vks.push(part_stark_vk); - } - } - Ok(requested_part_stark_vks) -} - impl ProofBuilder for OperatorProofBuilder { fn client(&self) -> &zkm_sdk::ProverClient { &self.client @@ -317,10 +372,10 @@ impl ProofBuilder for OperatorProofBuilder { operator_committed_blockhash, - watchtower_challenge_txns, - watchtower_challenge_txn_prev_outs, - watchtower_challenge_txn_pubkeys, - watchtower_challenge_txn_scripts, + graph_watchtower_xonly_public_keys, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, .. } = ctx else { @@ -330,12 +385,12 @@ impl ProofBuilder for OperatorProofBuilder { // --- header chain --- // let header_chain_input = { let zkm_public_values = - fs::read(&format!("{}.public_inputs.bin", header_chain_input_proof)).unwrap(); + fs::read(format!("{}.public_inputs.bin", header_chain_input_proof)).unwrap(); let zkm_proof = fs::read(header_chain_input_proof) .context("Failed to read input proof file") .unwrap(); let zkm_vk_hash = - fs::read(&format!("{}.vk_hash.bin", header_chain_input_proof)).unwrap(); + fs::read(format!("{}.vk_hash.bin", header_chain_input_proof)).unwrap(); let version_path = format!("{header_chain_input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| format!("failed to read zkm_version file '{version_path}'")) @@ -344,12 +399,15 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; HeaderChainCircuitInput { prev_proof: HeaderChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers: vec![], } }; @@ -357,12 +415,12 @@ impl ProofBuilder for OperatorProofBuilder { // --- commit chain --- // let commit_chain_input = { let zkm_public_values = - fs::read(&format!("{}.public_inputs.bin", commit_chain_input_proof)).unwrap(); + fs::read(format!("{}.public_inputs.bin", commit_chain_input_proof)).unwrap(); let zkm_proof = fs::read(commit_chain_input_proof) .context("Failed to read input proof file") .unwrap(); let zkm_vk_hash = - fs::read(&format!("{}.vk_hash.bin", commit_chain_input_proof)).unwrap(); + fs::read(format!("{}.vk_hash.bin", commit_chain_input_proof)).unwrap(); let version_path = format!("{commit_chain_input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| format!("failed to read zkm_version file '{version_path}'")) @@ -371,12 +429,15 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; CommitChainCircuitInput { prev_proof: CommitChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, commits: vec![], } }; @@ -387,9 +448,8 @@ impl ProofBuilder for OperatorProofBuilder { .context("Failed to read input proof file") .unwrap(); let zkm_public_values = - fs::read(&format!("{}.public_inputs.bin", state_chain_input_proof)).unwrap(); - let zkm_vk_hash = - fs::read(&format!("{}.vk_hash.bin", state_chain_input_proof)).unwrap(); + fs::read(format!("{}.public_inputs.bin", state_chain_input_proof)).unwrap(); + let zkm_vk_hash = fs::read(format!("{}.vk_hash.bin", state_chain_input_proof)).unwrap(); let version_path = format!("{state_chain_input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| format!("failed to read zkm_version file '{version_path}'")) @@ -398,6 +458,8 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; StateChainCircuitInput { prev_proof: StateChainPrevProofType::GenesisBlock, // unused @@ -405,35 +467,18 @@ impl ProofBuilder for OperatorProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: vec![], } }; - let header_chain_output: BlockHeaderCircuitOutput = - load_proof_public_output(header_chain_input_proof)?; - let state_chain_output: StateChainCircuitOutput = - load_proof_public_output(state_chain_input_proof)?; - let requested_part_stark_vks = collect_requested_part_stark_vks( - &header_chain_input, - &header_chain_output, - &state_chain_input, - &state_chain_output, - watchtower_challenge_txns, - )?; - let attestation_dir = part_stark_vk_attestation_dir(); - let (unique_witnesses, _) = - load_unique_part_stark_vk_witnesses(&attestation_dir, &requested_part_stark_vks) - .map_err(anyhow::Error::msg)?; - let attestation_inputs = OperatorAttestationInputs { unique_witnesses }; - // --- spv --- // //let latest_sequencer_commit_txid = Txid::from_str(&latest_sequencer_commit_txid).unwrap(); - let operator_genesis_sequencer_commit_txid = - Txid::from_str(&genesis_sequencer_commit_txid)?; + let operator_genesis_sequencer_commit_txid = Txid::from_str(genesis_sequencer_commit_txid)?; let bitcoin_block_headers = { - let headers: Vec = std::fs::read(&format!("{header_chain_input_proof}.blocks")) + let headers: Vec = std::fs::read(format!("{header_chain_input_proof}.blocks")) .context("read header chain blocks error")?; headers .chunks(80) @@ -457,34 +502,53 @@ impl ProofBuilder for OperatorProofBuilder { operator_latest_sequencer_commit_txn.compute_txid() ); let spv_ss_commit = build_spv( - &operator_latest_sequencer_commit_txn, + operator_latest_sequencer_commit_txn, *block_pos_ss_commit, target_block_ss_commit.clone(), &bitcoin_block_headers, ); + let watchtower_challenges = watchtower_challenge_witnesses + .iter() + .map(|(node_index, block_height, block, transaction)| { + anyhow::ensure!( + bitcoin_block_headers + .get(*block_height as usize) + .map(CircuitBlockHeader::compute_block_hash) + == Some(*block.block_hash().as_byte_array()), + "watchtower challenge block height is not in the authenticated header archive" + ); + Ok(IndexedWatchtowerChallenge { + node_index: *node_index, + spv: build_spv( + transaction, + *block_height, + block.clone(), + &bitcoin_block_headers, + ), + }) + }) + .collect::>>()?; // Generate the proofs let (proof, cycles, proving_time) = tracing::info_span!("generate proof").in_scope( || -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { let mut stdin = ZKMStdin::new(); - let included_watchtowers: U256 = U256::from_str(&included_watchtowers).unwrap(); + let included_watchtowers: U256 = U256::from_str(included_watchtowers).unwrap(); stdin.write(&included_watchtowers); stdin.write(&graph_id); stdin.write(&operator_genesis_sequencer_commit_txid.to_byte_array()); - stdin.write(&operator_latest_sequencer_commit_txn); - stdin.write(&watchtower_challenge_txns); - stdin.write(&watchtower_challenge_txn_pubkeys); - stdin.write(&watchtower_challenge_txn_scripts); - stdin.write(&watchtower_challenge_txn_prev_outs); + stdin.write(&watchtower_challenge_init_txid.to_byte_array()); + stdin.write(&watchtower_challenge_init_txn); + stdin.write(&graph_watchtower_xonly_public_keys); + stdin.write(&watchtower_challenges); stdin.write(&header_chain_input); stdin.write(&commit_chain_input); stdin.write(&state_chain_input); - stdin.write(&attestation_inputs); stdin.write(&spv_ss_commit); stdin.write(&operator_committed_blockhash.to_byte_array()); @@ -525,11 +589,11 @@ impl ProofBuilder for OperatorProofBuilder { let public_value_hex = hex::encode(proof.public_values.to_vec()); let proof_size = proof.bytes().len(); let zkm_version = proof.zkm_version.clone(); - std::fs::write(&format!("{}.public_inputs.bin", output), proof.public_values.to_vec())?; - std::fs::write(&format!("{}.vk_hash.bin", output), self.verifying_key.bytes32())?; - std::fs::write(&format!("{}.zkm_version.bin", output), zkm_version)?; - let proof = bincode::serialize(&proof).unwrap(); - std::fs::write(&format!("{}", output), proof)?; + std::fs::write(format!("{}.public_inputs.bin", output), proof.public_values.to_vec())?; + std::fs::write(format!("{}.vk_hash.bin", output), self.verifying_key.bytes32())?; + std::fs::write(format!("{}.zkm_version.bin", output), zkm_version)?; + let proof = bincode::serialize(&proof)?; + std::fs::write(output, proof)?; Ok((public_value_hex, proof_size)) } } @@ -540,83 +604,8 @@ mod tests { use ark_bn254::Bn254; use ark_groth16::{Groth16, r1cs_to_qap::LibsnarkReduction}; - use std::panic::{AssertUnwindSafe, catch_unwind}; - - use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk}; - - fn sample_header_input(zkm_version: &str) -> HeaderChainCircuitInput { - HeaderChainCircuitInput { - prev_proof: HeaderChainPrevProofType::GenesisBlock, - zkm_proof: vec![], - zkm_public_values: vec![], - zkm_vk_hash: vec![], - zkm_version: zkm_version.to_string(), - block_headers: vec![], - } - } - - fn sample_state_input(zkm_version: &str) -> StateChainCircuitInput { - StateChainCircuitInput { - prev_proof: StateChainPrevProofType::GenesisBlock, - zkm_proof: vec![], - zkm_public_values: vec![], - zkm_vk_hash: vec![], - zkm_version: zkm_version.to_string(), - blocks: vec![], - } - } - fn sample_header_output(part_stark_vk: Vec) -> BlockHeaderCircuitOutput { - BlockHeaderCircuitOutput { chain_state: header_chain::ChainState::new(), part_stark_vk } - } - - fn sample_state_output(part_stark_vk: Vec) -> StateChainCircuitOutput { - StateChainCircuitOutput { - chain_state: state_chain::StateChainState::new(0, [0u8; 32], Vec::new()), - part_stark_vk, - } - } - - #[test] - fn test_collect_requested_part_stark_vks_includes_outer_inner_and_watchtower_keys() { - let old_vk = load_part_stark_vk("v1.2.4").unwrap(); - let new_vk = load_part_stark_vk("v1.2.5").unwrap(); - - let graph_id = [7u8; 16]; - let proof = vec![3u8; 260]; - let public_inputs = vec![9u8; 36]; - let vk_hash = "ab".repeat(33); - let watchtower_comm = bitcoin_light_client_circuit::build_watchtower_commitment( - &graph_id, - &proof, - &public_inputs, - &vk_hash, - &new_vk, - ) - .unwrap(); - let watchtower_tx = Transaction { - version: bitcoin::transaction::Version::TWO, - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { - value: bitcoin::Amount::ZERO, - script_pubkey: bitcoin::ScriptBuf::new_op_return( - bitcoin::script::PushBytesBuf::try_from(watchtower_comm).unwrap(), - ), - }], - }; - - let requested = collect_requested_part_stark_vks( - &sample_header_input("v1.2.5"), - &sample_header_output(old_vk.clone()), - &sample_state_input("v1.2.5"), - &sample_state_output(old_vk.clone()), - &[watchtower_tx], - ) - .unwrap(); - - assert_eq!(requested, vec![new_vk.clone(), old_vk.clone(), new_vk.clone(), old_vk, new_vk]); - } + use zkm_verifier::{IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk}; #[tokio::test] #[ignore = "local test"] @@ -627,8 +616,11 @@ mod tests { let proof: ZKMProofWithPublicValues = bincode::deserialize(&proof_bytes).unwrap(); - let a: bitcoin_light_client_circuit::OperatorPublicOutputs = - proof.public_values.clone().read(); + let vk_hash = String::from_utf8(vk_bytes).unwrap(); + let a = bitcoin_light_client_circuit::decode_operator_public_outputs( + proof.public_values.as_slice(), + ) + .unwrap(); println!( "block hash: {:?}, constant: {:?}, included map: {:?}", hex::encode(a.btc_best_block_hash), @@ -636,14 +628,7 @@ mod tests { U256::from_le_bytes(a.included_watchtowers) ); - let vk_hash = String::from_utf8(vk_bytes).unwrap(); - let part_stark_vk = catch_unwind(AssertUnwindSafe(|| { - Groth16Verifier::get_part_stark_vk(&proof.zkm_version) - })) - .map_err(|_| { - anyhow::anyhow!("Failed to load part_stark_vk for zkm_version {}", proof.zkm_version) - }) - .unwrap(); + let part_stark_vk = zkm_verifier::Groth16Verifier::get_part_stark_vk(&proof.zkm_version); let ark_proof = convert_ark_imm_wrap_vk(&proof, &vk_hash, &IMM_GROTH16_VK_BYTES, part_stark_vk) .unwrap(); diff --git a/circuits/operator-proof/host/src/main.rs b/circuits/operator-proof/host/src/main.rs index 145bca9f0..dab623f7b 100644 --- a/circuits/operator-proof/host/src/main.rs +++ b/circuits/operator-proof/host/src/main.rs @@ -3,6 +3,7 @@ use clap::Parser; use operator_proof::{Args, OperatorProofBuilder, fetch_target_block_and_watchtower_tx}; use proof_builder::{ProofBuilder, ProofRequest}; use util::hex_parse; +use zkm_sdk::HashableKey; #[tokio::main] async fn main() { @@ -11,15 +12,22 @@ async fn main() { // Setup the logger. zkm_sdk::utils::setup_logger(); + let builder = OperatorProofBuilder::new(); + if args.print_program_id { + println!("OPERATOR_PROGRAM_ID={}", hex::encode(builder.program_id().unwrap())); + eprintln!("OPERATOR_VK_HASH={}", builder.vk().bytes32()); + return; + } + let ( block_pos_ss_commit, target_block_ss_commit, operator_committed_blockhash, operator_latest_sequencer_commit_txn, - watchtower_challenge_txns, - watchtower_challenge_txn_prev_outs, - watchtower_challenge_txn_pubkeys, - watchtower_challenge_txn_scripts, + graph_watchtower_xonly_public_keys, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, ) = fetch_target_block_and_watchtower_tx( &args.esplora_url, &args.latest_sequencer_commit_txid, @@ -32,8 +40,6 @@ async fn main() { .await .unwrap(); - let builder = OperatorProofBuilder::new(); - let ctx = ProofRequest::OperatorProofRequest { included_watchtowers: args.included_watchtowers.clone(), graph_id: hex_parse::<16>(&args.graph_id).unwrap(), @@ -51,10 +57,10 @@ async fn main() { operator_latest_sequencer_commit_txn, operator_committed_blockhash, - watchtower_challenge_txns, - watchtower_challenge_txn_prev_outs, - watchtower_challenge_txn_pubkeys, - watchtower_challenge_txn_scripts, + graph_watchtower_xonly_public_keys, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, }; let (input, proof, cycles, _) = builder.build_proof(&ctx).unwrap(); tracing::info!("Operator proof cycles: {cycles}"); diff --git a/circuits/proof-builder/Cargo.toml b/circuits/proof-builder/Cargo.toml index 4f6308738..0989dff91 100644 --- a/circuits/proof-builder/Cargo.toml +++ b/circuits/proof-builder/Cargo.toml @@ -8,17 +8,22 @@ anyhow = { workspace = true } thiserror = { workspace = true } sha2 = { workspace = true } serde.workspace = true +serde_json.workspace = true +secp256k1.workspace = true +hex.workspace = true +rand.workspace = true # Ziren zkm-sdk.workspace = true -zkm-prover.workspace = true -zkm-verifier.workspace = true +#zkm-prover.workspace = true +#zkm-verifier.workspace = true +verifier.workspace = true strum = { workspace = true, features = ["derive"] } # header-chain.workspace = true commit-chain.workspace = true -bitcoin-light-client-circuit.workspace = true +#bitcoin-light-client-circuit.workspace = true state-chain.workspace = true bitcoin = { workspace = true } diff --git a/circuits/proof-builder/src/api_auth.rs b/circuits/proof-builder/src/api_auth.rs new file mode 100644 index 000000000..19cb70fd4 --- /dev/null +++ b/circuits/proof-builder/src/api_auth.rs @@ -0,0 +1,301 @@ +use anyhow::{Context, bail}; +use rand::{RngCore, rngs::OsRng}; +use secp256k1::schnorr::Signature; +use secp256k1::{Keypair, Message, PublicKey, SECP256K1, XOnlyPublicKey}; +use serde::Serialize; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use std::mem; +use std::str::FromStr; + +pub const AUTH_TIMESTAMP_HEADER: &str = "x-proof-auth-timestamp"; +pub const AUTH_NONCE_HEADER: &str = "x-proof-auth-nonce"; +pub const AUTH_PUBLIC_KEY_HEADER: &str = "x-proof-auth-public-key"; +pub const AUTH_SIGNATURE_HEADER: &str = "x-proof-auth-signature"; +pub const AUTH_WINDOW_SECS: i64 = 300; + +const AUTH_DOMAIN: &str = "bitvm-proof-builder-auth-v1"; +const AUTH_NONCE_LEN: usize = 16; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProofBuilderAuthRole { + Operator, + Watchtower, +} + +impl ProofBuilderAuthRole { + /// Returns the stable role label included in the signed payload. + pub fn as_str(self) -> &'static str { + match self { + Self::Operator => "operator", + Self::Watchtower => "watchtower", + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ProofBuilderAuthHeaders { + pub timestamp: String, + pub nonce: String, + pub public_key: String, + pub signature: String, +} + +impl ProofBuilderAuthHeaders { + /// Converts the signed values into HTTP header name/value pairs. + pub fn to_header_pairs(&self) -> Vec<(String, String)> { + vec![ + (AUTH_TIMESTAMP_HEADER.to_string(), self.timestamp.clone()), + (AUTH_NONCE_HEADER.to_string(), self.nonce.clone()), + (AUTH_PUBLIC_KEY_HEADER.to_string(), self.public_key.clone()), + (AUTH_SIGNATURE_HEADER.to_string(), self.signature.clone()), + ] + } +} + +/// Normalizes a compressed or x-only secp256k1 public key to x-only form. +pub fn normalize_public_key(value: &str) -> anyhow::Result { + if let Ok(public_key) = XOnlyPublicKey::from_str(value) { + return Ok(public_key); + } + let public_key = PublicKey::from_str(value).context("invalid secp256k1 public key")?; + Ok(public_key.x_only_public_key().0) +} + +/// Signs one Proof Builder request with a fresh nonce and the caller's node key. +#[allow(clippy::too_many_arguments)] +pub fn sign_proof_builder_request( + keypair: &Keypair, + role: ProofBuilderAuthRole, + method: &str, + path: &str, + body: &B, +) -> anyhow::Result { + let timestamp = current_time_secs().to_string(); + let mut nonce = [0u8; AUTH_NONCE_LEN]; + OsRng.fill_bytes(&mut nonce); + let nonce = hex::encode(nonce); + let public_key = keypair.x_only_public_key().0.to_string(); + let digest = request_digest(role, method, path, ×tamp, &nonce, &public_key, body)?; + let mut auxiliary_randomness = [0u8; 32]; + OsRng.fill_bytes(&mut auxiliary_randomness); + let signature = SECP256K1.sign_schnorr_with_aux_rand( + &Message::from_digest(digest), + keypair, + &auxiliary_randomness, + ); + + Ok(ProofBuilderAuthHeaders { + timestamp, + nonce, + public_key, + signature: hex::encode(signature.as_ref()), + }) +} + +/// Verifies the cryptographic binding of one Proof Builder request. +#[allow(clippy::too_many_arguments)] +pub fn verify_proof_builder_request_signature( + role: ProofBuilderAuthRole, + method: &str, + path: &str, + timestamp: &str, + nonce: &str, + public_key: &XOnlyPublicKey, + signature: &str, + body: &B, +) -> anyhow::Result<()> { + validate_timestamp(timestamp)?; + let nonce_bytes = hex::decode(nonce).context("invalid auth nonce encoding")?; + if nonce_bytes.len() != AUTH_NONCE_LEN { + bail!("invalid auth nonce length"); + } + let signature_bytes = hex::decode(signature).context("invalid auth signature encoding")?; + let signature = Signature::from_slice(&signature_bytes).context("invalid auth signature")?; + let canonical_public_key = public_key.to_string(); + let digest = request_digest(role, method, path, timestamp, nonce, &canonical_public_key, body)?; + SECP256K1 + .verify_schnorr(&signature, &Message::from_digest(digest), public_key) + .context("auth signature verification failed") +} + +/// Validates that an authentication timestamp is canonical and within the accepted window. +pub fn validate_timestamp(timestamp: &str) -> anyhow::Result { + let timestamp_value: i64 = timestamp.parse().context("invalid auth timestamp")?; + if timestamp_value.to_string() != timestamp { + bail!("auth timestamp is not canonical"); + } + let now = current_time_secs(); + if (now - timestamp_value).abs() > AUTH_WINDOW_SECS { + bail!("auth timestamp expired"); + } + Ok(timestamp_value) +} + +/// Hashes length-prefixed request fields and canonical JSON body bytes into the signed digest. +fn request_digest( + role: ProofBuilderAuthRole, + method: &str, + path: &str, + timestamp: &str, + nonce: &str, + public_key: &str, + body: &B, +) -> anyhow::Result<[u8; 32]> { + let body_hash = Sha256::digest(canonical_json_bytes(body)?); + let mut hasher = Sha256::new(); + for field in [AUTH_DOMAIN, role.as_str(), method, path, timestamp, nonce, public_key] { + hash_field(&mut hasher, field.as_bytes()); + } + hash_field(&mut hasher, &body_hash); + Ok(hasher.finalize().into()) +} + +/// Serializes JSON objects with recursively sorted keys for cross-client digest stability. +fn canonical_json_bytes(body: &B) -> anyhow::Result> { + let mut value = + serde_json::to_value(body).context("failed to serialize authenticated request body")?; + sort_json_value(&mut value); + serde_json::to_vec(&value).context("failed to encode canonical authenticated request body") +} + +/// Recursively sorts JSON object keys while preserving array order. +fn sort_json_value(value: &mut Value) { + match value { + Value::Object(map) => { + let mut entries = mem::take(map).into_iter().collect::>(); + entries.sort_unstable_by(|left, right| left.0.cmp(&right.0)); + for (key, mut value) in entries { + sort_json_value(&mut value); + map.insert(key, value); + } + } + Value::Array(values) => values.iter_mut().for_each(sort_json_value), + _ => {} + } +} + +/// Adds one length-prefixed field to the authentication digest. +fn hash_field(hasher: &mut Sha256, field: &[u8]) { + hasher.update((field.len() as u64).to_be_bytes()); + hasher.update(field); +} + +/// Returns the current Unix time used by the authentication freshness check. +fn current_time_secs() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time should be after unix epoch") + .as_secs() as i64 +} + +#[cfg(test)] +mod tests { + use super::*; + use serde::Serialize; + + #[derive(Serialize)] + struct TestBody { + graph_id: &'static str, + value: u64, + } + + #[derive(Serialize)] + struct ReorderedTestBody { + value: u64, + graph_id: &'static str, + } + + fn keypair(seed: u8) -> Keypair { + Keypair::from_seckey_slice(SECP256K1, &[seed; 32]).unwrap() + } + + #[test] + fn signed_request_verifies_and_is_bound_to_request() { + let keypair = keypair(7); + let body = TestBody { graph_id: "graph-1", value: 1 }; + let headers = sign_proof_builder_request( + &keypair, + ProofBuilderAuthRole::Operator, + "POST", + "/v1/proofs/operator_proofs", + &body, + ) + .unwrap(); + let public_key = normalize_public_key(&headers.public_key).unwrap(); + + assert!( + verify_proof_builder_request_signature( + ProofBuilderAuthRole::Operator, + "POST", + "/v1/proofs/operator_proofs", + &headers.timestamp, + &headers.nonce, + &public_key, + &headers.signature, + &body, + ) + .is_ok() + ); + assert!( + verify_proof_builder_request_signature( + ProofBuilderAuthRole::Operator, + "POST", + "/v1/proofs/operator_proofs_timeout", + &headers.timestamp, + &headers.nonce, + &public_key, + &headers.signature, + &body, + ) + .is_err() + ); + assert!( + verify_proof_builder_request_signature( + ProofBuilderAuthRole::Watchtower, + "POST", + "/v1/proofs/operator_proofs", + &headers.timestamp, + &headers.nonce, + &public_key, + &headers.signature, + &body, + ) + .is_err() + ); + assert!( + verify_proof_builder_request_signature( + ProofBuilderAuthRole::Operator, + "POST", + "/v1/proofs/operator_proofs", + &headers.timestamp, + &headers.nonce, + &public_key, + &headers.signature, + &TestBody { graph_id: "graph-2", value: 1 }, + ) + .is_err() + ); + assert!( + verify_proof_builder_request_signature( + ProofBuilderAuthRole::Operator, + "POST", + "/v1/proofs/operator_proofs", + &headers.timestamp, + &headers.nonce, + &public_key, + &headers.signature, + &ReorderedTestBody { value: 1, graph_id: "graph-1" }, + ) + .is_ok() + ); + } + + #[test] + fn public_key_normalization_accepts_compressed_and_x_only_keys() { + let keypair = keypair(9); + let expected = keypair.x_only_public_key().0; + assert_eq!(normalize_public_key(&expected.to_string()).unwrap(), expected); + assert_eq!(normalize_public_key(&keypair.public_key().to_string()).unwrap(), expected); + } +} diff --git a/circuits/proof-builder/src/lib.rs b/circuits/proof-builder/src/lib.rs index 710d90987..5d76ffea5 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use bitcoin::{Block, BlockHash, ScriptBuf, Transaction, TxOut}; +use bitcoin::{Block, BlockHash, Transaction, Txid}; use commit_chain::CircuitCommit; use header_chain::CircuitBlockHeader; use serde::{Deserialize, Serialize}; @@ -7,9 +7,11 @@ use state_chain::CircuitStateBlock; use std::fs; use strum::{Display, EnumString}; use thiserror::Error; -use zkm_sdk::{ProverClient, ZKMProofWithPublicValues}; +use zkm_sdk::{HashableKey, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofWithPublicValues}; use zkm_sdk::{ZKMProvingKey, ZKMVerifyingKey}; +pub mod api_auth; + #[derive(Debug, Clone, Serialize, Deserialize)] pub enum ProofRequest { HeaderChainProofRequest { @@ -63,10 +65,10 @@ pub enum ProofRequest { operator_committed_blockhash: BlockHash, - watchtower_challenge_txns: Vec, - watchtower_challenge_txn_prev_outs: Vec, - watchtower_challenge_txn_pubkeys: Vec, - watchtower_challenge_txn_scripts: Vec, + graph_watchtower_xonly_public_keys: Vec<[u8; 32]>, + watchtower_challenge_init_txid: Txid, + watchtower_challenge_init_txn: Option, + watchtower_challenge_witnesses: Vec<(u16, u32, Block, Transaction)>, }, } @@ -85,6 +87,12 @@ pub trait ProofBuilder { fn pk(&self) -> &ZKMProvingKey; fn vk(&self) -> &ZKMVerifyingKey; + /// Returns the Program ID derived from the builder's verifying key. + fn program_id(&self) -> Result { + verifier::program_id(self.vk().bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } + fn build_proof( &self, ctx: &ProofRequest, @@ -114,7 +122,7 @@ pub struct OnDemandTask { pub state_chain_input_proof: String, pub watchtower_challenge_init_txid: Option, - pub watchtower_challenge_txids: Vec, + pub watchtower_challenge_txids: Vec>, pub included_watchtowers: Vec, pub watchtower_public_keys: Vec, pub graph_id: Option, @@ -192,9 +200,11 @@ pub struct ProofDescResponse { pub struct OperatorProofRequest { pub instance_id: String, pub graph_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gateway_address: Option, pub operator_committed_blockhash: String, pub execution_layer_block_number: i64, - pub watchtower_challenge_txids: Vec, + pub watchtower_challenge_txids: Vec>, pub included_watchtowers: Vec, pub watchtower_challenge_init_txid: String, pub watchtower_challenge_pubkeys: Vec, @@ -206,7 +216,6 @@ pub struct ProofData { pub vk: String, pub public_inputs: Vec, pub zkm_version: String, - pub proof_part_stark_vk: Vec, } impl ProofData { @@ -228,8 +237,6 @@ impl ProofData { fs::read(format!("{path}.zkm_version.bin")).unwrap_or_default(), ) .unwrap_or_default(); - proof_data.proof_part_stark_vk = - fs::read(format!("{path}.proof_part_stark_vk.bin")).unwrap_or_default(); } } proof_data @@ -246,6 +253,8 @@ pub struct OperatorProofResponse { pub struct WatchtowerProofRequest { pub instance_id: String, pub graph_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gateway_address: Option, pub public_key: String, pub challenge_init_txid: String, pub execution_layer_block_number: i64, @@ -261,6 +270,8 @@ pub struct WatchtowerProofResponse { pub struct OperatorProofTimeoutUpdateRequest { pub instance_id: String, pub graph_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gateway_address: Option, } #[derive(Debug, Serialize, Deserialize)] pub struct OperatorProofTimeoutUpdateResponse { @@ -274,6 +285,8 @@ pub struct OperatorProofTimeoutUpdateResponse { pub struct WatchtowerProofTimeoutUpdateRequest { pub instance_id: String, pub graph_id: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub gateway_address: Option, pub public_key: String, } @@ -285,40 +298,3 @@ pub struct WatchtowerProofTimeoutUpdateResponse { pub data: Option, pub error: Option, } - -#[cfg(test)] -mod tests { - use super::*; - use std::path::PathBuf; - use std::time::{SystemTime, UNIX_EPOCH}; - - fn temp_proof_base() -> PathBuf { - let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); - std::env::temp_dir().join(format!("proof-data-test-{nanos}")) - } - - #[test] - fn load_proof_data_reads_proof_part_stark_vk_sidecar() { - let base = temp_proof_base(); - let base_str = base.to_string_lossy().to_string(); - fs::write(&base, [1u8, 2, 3]).unwrap(); - fs::write(format!("{base_str}.public_inputs.bin"), [4u8, 5, 6]).unwrap(); - fs::write(format!("{base_str}.vk_hash.bin"), b"vk-hash").unwrap(); - fs::write(format!("{base_str}.zkm_version.bin"), b"v1.2.5").unwrap(); - fs::write(format!("{base_str}.proof_part_stark_vk.bin"), [9u8, 8, 7]).unwrap(); - - let proof_data = ProofData::load_proof_data(&base_str, ProofType::Watchtower); - - assert_eq!(proof_data.proof, vec![1u8, 2, 3]); - assert_eq!(proof_data.public_inputs, vec![4u8, 5, 6]); - assert_eq!(proof_data.vk, "vk-hash"); - assert_eq!(proof_data.zkm_version, "v1.2.5"); - assert_eq!(proof_data.proof_part_stark_vk, vec![9u8, 8, 7]); - - let _ = fs::remove_file(&base); - let _ = fs::remove_file(format!("{base_str}.public_inputs.bin")); - let _ = fs::remove_file(format!("{base_str}.vk_hash.bin")); - let _ = fs::remove_file(format!("{base_str}.zkm_version.bin")); - let _ = fs::remove_file(format!("{base_str}.proof_part_stark_vk.bin")); - } -} diff --git a/circuits/state-chain-proof/guest/Cargo.lock b/circuits/state-chain-proof/guest/Cargo.lock index 612d3e5b3..f9ba5bda7 100644 --- a/circuits/state-chain-proof/guest/Cargo.lock +++ b/circuits/state-chain-proof/guest/Cargo.lock @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "alloy-primitives" version = "1.4.1" -source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#59313700f710f1373a5b4cdc05c51dc3227d8064" +source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#0d034202c9977acd601399efdd9c30fd1c1a08a0" dependencies = [ "alloy-rlp", "bytes", @@ -1069,6 +1069,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1103,7 +1117,7 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#bcf9397426410acc171ad625416b508dc913be8e" +source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#97b215f80e9bfab9ad6641e41b005b6d8e58f748" dependencies = [ "cfg-if", "ff 0.13.1", @@ -1261,7 +1275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -1334,6 +1348,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -1415,7 +1438,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version 0.4.1", @@ -2225,14 +2248,13 @@ dependencies = [ "sha2 0.10.9", "state-chain", "tracing", - "zkm-verifier", "zkm-zkvm", ] [[package]] name = "guest-executor" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-consensus", "alloy-evm", @@ -2376,14 +2398,16 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -2749,7 +2773,7 @@ dependencies = [ [[package]] name = "k256" version = "0.13.4" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#8266b228a39402a0ba68d644b7f26b85b5112fe3" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#6ad84b9b604911c6f5dd353b88c04927d2739a32" dependencies = [ "cfg-if", "ecdsa", @@ -2768,7 +2792,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -2913,7 +2937,7 @@ dependencies = [ [[package]] name = "mpt" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -3170,7 +3194,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p256" version = "0.13.2" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "ecdsa", "elliptic-curve", @@ -3729,7 +3753,7 @@ dependencies = [ [[package]] name = "primeorder" version = "0.13.1" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "elliptic-curve", ] @@ -3748,7 +3772,7 @@ dependencies = [ [[package]] name = "primitives" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-eips", "alloy-genesis", @@ -4999,7 +5023,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -5010,7 +5034,7 @@ version = "0.10.9" source = "git+https://github.com/ziren-patches/RustCrypto-hashes?branch=patch-sha2-0.10.9#dbfdbd088ac7b55e03c0b9eb192278efe35d67dc" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5114,7 +5138,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -5135,7 +5159,8 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5203,7 +5228,7 @@ dependencies = [ [[package]] name = "substrate-bn" version = "0.6.0" -source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" +source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#0dfeeb1b7bfc21941b6e4964d678abd094a54a6f" dependencies = [ "bytemuck", "byteorder", @@ -5704,6 +5729,14 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "sha2 0.10.9", + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" @@ -6272,8 +6305,8 @@ dependencies = [ [[package]] name = "zkm-core-executor" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6313,8 +6346,8 @@ dependencies = [ [[package]] name = "zkm-core-machine" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6365,8 +6398,8 @@ dependencies = [ [[package]] name = "zkm-curves" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "cfg-if", "curve25519-dalek", @@ -6389,8 +6422,8 @@ dependencies = [ [[package]] name = "zkm-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "proc-macro2", "quote", @@ -6399,8 +6432,8 @@ dependencies = [ [[package]] name = "zkm-lib" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6412,10 +6445,11 @@ dependencies = [ [[package]] name = "zkm-primitives" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -6426,12 +6460,13 @@ dependencies = [ "p3-symmetric", "serde", "sha2 0.10.9", + "tracing", ] [[package]] name = "zkm-recursion-core" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "backtrace", "ff 0.13.1", @@ -6467,8 +6502,8 @@ dependencies = [ [[package]] name = "zkm-stark" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -6509,8 +6544,8 @@ dependencies = [ [[package]] name = "zkm-verifier" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6539,10 +6574,11 @@ dependencies = [ [[package]] name = "zkm-zkvm" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "cfg-if", "getrandom 0.2.17", "lazy_static", diff --git a/circuits/state-chain-proof/guest/Cargo.toml b/circuits/state-chain-proof/guest/Cargo.toml index 9e195a812..5dbf2607e 100644 --- a/circuits/state-chain-proof/guest/Cargo.toml +++ b/circuits/state-chain-proof/guest/Cargo.toml @@ -11,9 +11,7 @@ bincode = "1.3.3" state-chain = { path = "../../../crates/state-chain" } # Ziren -zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } -zkm-verifier = { git = "https://github.com/ProjectZKM/Ziren" } -#zkm-verifier = { path = "../../../Ziren/crates/verifier" } +zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7", features = ["verify"] } #zkm-zkvm = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging @@ -35,3 +33,6 @@ alloy-primitives-v1-1-2 = { git = "https://github.com/ziren-patches/core.git", p alloy-primitives-v1-4-1 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.4.1" } secp256k1-v0-29-1 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.29.1" } #secp256k1-v0-30-0 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.30.0" } + +[features] +imm-wrap-vk = ["zkm-zkvm/imm-wrap-vk", "state-chain/imm-wrap-vk"] diff --git a/circuits/state-chain-proof/host/Cargo.toml b/circuits/state-chain-proof/host/Cargo.toml index da7404d89..3178e1e98 100644 --- a/circuits/state-chain-proof/host/Cargo.toml +++ b/circuits/state-chain-proof/host/Cargo.toml @@ -35,6 +35,7 @@ cbft-rpc.workspace = true proof-builder.workspace = true alloy-consensus.workspace = true util.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/state-chain-proof/host/src/lib.rs b/circuits/state-chain-proof/host/src/lib.rs index e908c95aa..40f4593c1 100644 --- a/circuits/state-chain-proof/host/src/lib.rs +++ b/circuits/state-chain-proof/host/src/lib.rs @@ -17,8 +17,8 @@ use state_chain::*; use std::sync::Arc; use url::Url; use zkm_sdk::{ - HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, - include_elf, + HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofKind, ZKMProofWithPublicValues, + ZKMStdin, include_elf, }; use sha2::{Digest, Sha256}; @@ -36,6 +36,10 @@ use clap::Parser; /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -66,11 +70,24 @@ pub struct Args { #[clap(long, env, default_value_t = 0)] pub start: u64, - #[clap(long, env)] + // Print-only mode skips runtime inputs but keeps them required otherwise. + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub l2_contract_addresses: String, // https://explorer.testnet3.goat.network/address/0x9F0A61ce47678F43A326dB9F8964C56a924cd3D0?tab=read_write_contract - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub proceed_withdraw_method_ids: String, } @@ -98,7 +115,7 @@ async fn fetch_withdrawal_events( start: u64, batch_size: u64, ) -> anyhow::Result> { - let rpc_url = Url::parse(&execution_layer_rpc)?; + let rpc_url = Url::parse(execution_layer_rpc)?; let provider = RootProvider::::new_http(rpc_url); let mut withdrawals = vec![]; for i in start..start + batch_size { @@ -150,7 +167,7 @@ async fn fetch_exection_layer_block( genesis: &Genesis, ) -> anyhow::Result { // Setup the provider. - let rpc_url = Url::parse(&execution_layer_rpc)?; + let rpc_url = Url::parse(execution_layer_rpc)?; let provider = RootProvider::::new_http(rpc_url); //let rpc_db = RpcDb::new(provider.clone(), provider.clone(), execution_layer_block_number - 1); let chain_spec: Arc = Arc::new(genesis.try_into().unwrap()); @@ -170,6 +187,7 @@ async fn fetch_exection_layer_block( Ok(client_input) } +#[allow(clippy::too_many_arguments)] pub async fn fetch_state_chain( l2_contract_addresses: &str, proceed_withdraw_method_ids: &str, @@ -194,7 +212,7 @@ pub async fn fetch_state_chain( let genesis = if genesis == "goattest" { Genesis::GoatTestnet } else { Genesis::GOAT }; assert!(start > 0, "Don't get genesis block from the consensus layer."); let mut blocks: Vec<_> = Vec::new(); - let base_slot: [u8; 32] = U256::from(16).to_be_bytes().try_into()?; + let base_slot: [u8; 32] = U256::from(16).to_be_bytes(); // fetch graph_block_numbers and graph_ids between in goat block(start, start + batch_size) let withdrawal_events = fetch_withdrawal_events( execution_layer_rpc, @@ -207,7 +225,7 @@ pub async fn fetch_state_chain( for i in start..(start + batch_size) { let evm_block = - fetch_exection_layer_block(&execution_layer_rpc, i, &genesis).await.map_err(|e| { + fetch_exection_layer_block(execution_layer_rpc, i, &genesis).await.map_err(|e| { tracing::error!("fetch_exection_layer_block: {e:?}"); proof_builder::ProofError::InputNotReady((batch_size + start - i) * 3) })?; @@ -216,10 +234,9 @@ pub async fn fetch_state_chain( evm_block.current_block.header.parent_beacon_block_root.unwrap(); let parent_cosmos_block_height = - match get_cosmos_block_height_at(cosmos_rpc_url, *parent_beacon_block_root).await { - Ok(d) => d, - Err(_) => None, - }; + get_cosmos_block_height_at(cosmos_rpc_url, *parent_beacon_block_root) + .await + .unwrap_or_default(); let (_, cl_block_number) = fetch_cbft_validator_info(cosmos_rpc_url, i, parent_cosmos_block_height, 1000) @@ -265,7 +282,7 @@ pub async fn fetch_state_chain( blocks.push(CircuitStateBlock { cosmos_txns, cosmos_block, evm_block, withdrawals }); } let block_bytes = serde_json::to_vec(&blocks)?; - std::fs::write(&blocks_file, block_bytes)?; + std::fs::write(blocks_file, block_bytes)?; Ok(blocks) } @@ -276,6 +293,7 @@ pub struct StateChainProofBuilder { } impl StateChainProofBuilder { + #[allow(clippy::new_without_default)] pub fn new() -> Self { let client = ProverClient::new(); let (proving_key, verifying_key) = client.setup(STATE_CHAIN); @@ -313,18 +331,19 @@ impl ProofBuilder for StateChainProofBuilder { let prev_receipt = if *init_input { None } else { - let public_inputs = fs::read(&format!("{}.public_inputs.bin", input_proof)) + let public_inputs = fs::read(format!("{}.public_inputs.bin", input_proof)) .context("Read public input")?; Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { let proof_bytes = fs::read(input_proof).context("Failed to read input proof file")?; - let zkm_vk_hash = fs::read(&format!("{}.vk_hash.bin", input_proof)) - .context("Read vk_hash")?; + let zkm_vk_hash = + fs::read(format!("{}.vk_hash.bin", input_proof)).context("Read vk_hash")?; let version_path = format!("{input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| { @@ -335,22 +354,16 @@ impl ProofBuilder for StateChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - let prev_output: StateChainCircuitOutput = - zkm_sdk::ZKMPublicValues::from(&public_inputs).read(); - ( - StateChainPrevProofType::PrevProof(prev_output), - proof_bytes, - public_inputs, - zkm_vk_hash.to_vec(), - zkm_version, - ) + let prev_proof = + classify_state_chain_output(&public_inputs).map_err(anyhow::Error::msg)?; + (prev_proof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), zkm_version) } None => ( StateChainPrevProofType::GenesisBlock, Vec::new(), Vec::new(), Vec::new(), - "v1.2.5".into(), + ZKM_CIRCUIT_VERSION.into(), ), }; @@ -360,6 +373,7 @@ impl ProofBuilder for StateChainProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: blocks.clone(), }; // Generate the proofs. @@ -389,9 +403,10 @@ impl ProofBuilder for StateChainProofBuilder { }, )?; tracing::info!("State chain proof cycles: {}", cycles); - if let Err(e) = self.client.verify(&proof, &self.verifying_key) { - panic!("{}", e); - } + + self.client + .verify(&proof, &self.verifying_key) + .context("Failed to verify generated state chain proof")?; let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) @@ -407,16 +422,16 @@ impl ProofBuilder for StateChainProofBuilder { let ProofRequest::StateChainProofRequest { output_proof, .. } = ctx else { anyhow::bail!("Invalid state chain inputs"); }; - std::fs::write(&format!("{}", output_proof), proof.bytes())?; + std::fs::write(output_proof, proof.bytes())?; let public_value_hex = hex::encode(proof.public_values.to_vec()); let proof_size = proof.bytes().len(); let zkm_version = proof.zkm_version.clone(); std::fs::write( - &format!("{}.public_inputs.bin", output_proof), + format!("{}.public_inputs.bin", output_proof), proof.public_values.to_vec(), )?; - std::fs::write(&format!("{}.vk_hash.bin", output_proof), self.verifying_key.bytes32())?; - std::fs::write(&format!("{}.zkm_version.bin", output_proof), zkm_version)?; + std::fs::write(format!("{}.vk_hash.bin", output_proof), self.verifying_key.bytes32())?; + std::fs::write(format!("{}.zkm_version.bin", output_proof), zkm_version)?; tracing::info!("Generate proof successfully, proof: {:?}", proof); Ok((public_value_hex, proof_size)) diff --git a/circuits/state-chain-proof/host/src/main.rs b/circuits/state-chain-proof/host/src/main.rs index 21b16511d..37069fcd8 100644 --- a/circuits/state-chain-proof/host/src/main.rs +++ b/circuits/state-chain-proof/host/src/main.rs @@ -11,6 +11,12 @@ async fn main() { tracing::info!("args: {:?}", args); // Setup the logger. zkm_sdk::utils::setup_logger(); + let builder = StateChainProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let blocks = fetch_state_chain( &args.l2_contract_addresses, &args.proceed_withdraw_method_ids, @@ -24,8 +30,6 @@ async fn main() { .await .unwrap(); - let builder = StateChainProofBuilder::new(); - let ctx = ProofRequest::StateChainProofRequest { init_input: args.init_input, input_proof: args.input_proof.clone(), diff --git a/circuits/watchtower-proof/guest/Cargo.lock b/circuits/watchtower-proof/guest/Cargo.lock index af3cab9fa..266ae80e4 100644 --- a/circuits/watchtower-proof/guest/Cargo.lock +++ b/circuits/watchtower-proof/guest/Cargo.lock @@ -276,7 +276,7 @@ dependencies = [ [[package]] name = "alloy-primitives" version = "1.4.1" -source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#59313700f710f1373a5b4cdc05c51dc3227d8064" +source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.4.1#0d034202c9977acd601399efdd9c30fd1c1a08a0" dependencies = [ "alloy-rlp", "bytes", @@ -1008,7 +1008,7 @@ checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" [[package]] name = "bitcoin-light-client-circuit" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1022,11 +1022,11 @@ dependencies = [ "revm-database-interface", "serde", "serde_json", - "sha2 0.10.9", "state-chain", "tendermint", "tendermint-light-client-verifier", "tracing", + "verifier", "zkm-primitives", "zkm-verifier", "zkm-zkvm", @@ -1095,6 +1095,20 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "blake3" +version = "1.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" +dependencies = [ + "arrayref", + "arrayvec", + "cc", + "cfg-if", + "constant_time_eq", + "cpufeatures 0.3.0", +] + [[package]] name = "block-buffer" version = "0.9.0" @@ -1129,7 +1143,7 @@ dependencies = [ [[package]] name = "bls12_381" version = "0.8.0" -source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#bcf9397426410acc171ad625416b508dc913be8e" +source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#97b215f80e9bfab9ad6641e41b005b6d8e58f748" dependencies = [ "cfg-if", "ff 0.13.1", @@ -1282,7 +1296,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commit-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1297,7 +1311,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -1308,7 +1322,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "531185e432bb31db1ecda541e9e7ab21468d4d844ad7505e0546a49b4945d49b" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "proptest", "serde_core", ] @@ -1381,6 +1395,15 @@ dependencies = [ "libc", ] +[[package]] +name = "cpufeatures" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201" +dependencies = [ + "libc", +] + [[package]] name = "crc" version = "3.4.0" @@ -1462,7 +1485,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "curve25519-dalek-derive", "fiat-crypto", "rustc_version 0.4.1", @@ -2276,14 +2299,13 @@ dependencies = [ "sha2 0.10.9", "state-chain", "tracing", - "zkm-verifier", "zkm-zkvm", ] [[package]] name = "guest-executor" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-consensus", "alloy-evm", @@ -2427,14 +2449,16 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -2800,7 +2824,7 @@ dependencies = [ [[package]] name = "k256" version = "0.13.4" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#8266b228a39402a0ba68d644b7f26b85b5112fe3" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#6ad84b9b604911c6f5dd353b88c04927d2739a32" dependencies = [ "cfg-if", "ecdsa", @@ -2819,7 +2843,7 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" dependencies = [ - "cpufeatures", + "cpufeatures 0.2.17", ] [[package]] @@ -2964,7 +2988,7 @@ dependencies = [ [[package]] name = "mpt" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-primitives", "alloy-rlp", @@ -3221,7 +3245,7 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "p256" version = "0.13.2" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "ecdsa", "elliptic-curve", @@ -3780,7 +3804,7 @@ dependencies = [ [[package]] name = "primeorder" version = "0.13.1" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#88d42225abcb639b9bd3a930ac5a14f2262541c8" dependencies = [ "elliptic-curve", ] @@ -3799,7 +3823,7 @@ dependencies = [ [[package]] name = "primitives" version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#eee4dbfbdb7734d2785d58f95de84393dbb9009e" dependencies = [ "alloy-eips", "alloy-genesis", @@ -5050,7 +5074,7 @@ checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" dependencies = [ "block-buffer 0.9.0", "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.9.0", "opaque-debug", ] @@ -5061,7 +5085,7 @@ version = "0.10.9" source = "git+https://github.com/ziren-patches/RustCrypto-hashes?branch=patch-sha2-0.10.9#dbfdbd088ac7b55e03c0b9eb192278efe35d67dc" dependencies = [ "cfg-if", - "cpufeatures", + "cpufeatures 0.2.17", "digest 0.10.7", ] @@ -5165,7 +5189,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -5186,7 +5210,8 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5254,7 +5279,7 @@ dependencies = [ [[package]] name = "substrate-bn" version = "0.6.0" -source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" +source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#0dfeeb1b7bfc21941b6e4964d678abd094a54a6f" dependencies = [ "bytemuck", "byteorder", @@ -5755,6 +5780,14 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "sha2 0.10.9", + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" @@ -6323,8 +6356,8 @@ dependencies = [ [[package]] name = "zkm-core-executor" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6364,8 +6397,8 @@ dependencies = [ [[package]] name = "zkm-core-machine" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6416,8 +6449,8 @@ dependencies = [ [[package]] name = "zkm-curves" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "cfg-if", "curve25519-dalek", @@ -6440,8 +6473,8 @@ dependencies = [ [[package]] name = "zkm-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "proc-macro2", "quote", @@ -6450,8 +6483,8 @@ dependencies = [ [[package]] name = "zkm-lib" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", "cfg-if", @@ -6463,10 +6496,11 @@ dependencies = [ [[package]] name = "zkm-primitives" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "hex", "lazy_static", "num-bigint 0.4.6", @@ -6477,12 +6511,13 @@ dependencies = [ "p3-symmetric", "serde", "sha2 0.10.9", + "tracing", ] [[package]] name = "zkm-recursion-core" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "backtrace", "ff 0.13.1", @@ -6518,8 +6553,8 @@ dependencies = [ [[package]] name = "zkm-stark" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -6560,8 +6595,8 @@ dependencies = [ [[package]] name = "zkm-verifier" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "anyhow", "bincode", @@ -6590,10 +6625,11 @@ dependencies = [ [[package]] name = "zkm-zkvm" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +version = "1.2.7" +source = "git+https://github.com/ProjectZKM/Ziren?tag=v1.2.7#7dc2037c6ef99d5772c756556350698fad01c559" dependencies = [ "bincode", + "blake3", "cfg-if", "getrandom 0.2.17", "lazy_static", diff --git a/circuits/watchtower-proof/guest/Cargo.toml b/circuits/watchtower-proof/guest/Cargo.toml index fca4952e4..69ee11a0b 100644 --- a/circuits/watchtower-proof/guest/Cargo.toml +++ b/circuits/watchtower-proof/guest/Cargo.toml @@ -14,9 +14,7 @@ state-chain = { path = "../../../crates/state-chain" } bitcoin-light-client-circuit = { path = "../../../crates/bitcoin-light-client-circuit" } # Ziren -zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } -zkm-verifier = { git = "https://github.com/ProjectZKM/Ziren" } -#zkm-verifier = { path = "../../../Ziren/crates/verifier" } +zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", tag = "v1.2.7", features = ["verify"] } #zkm-zkvm = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging @@ -43,3 +41,8 @@ secp256k1-v0-29-1 = { git = "https://github.com/ziren-patches/rust-secp256k1", p #secp256k1-v0-30-0 = { git = "https://github.com/ziren-patches/rust-secp256k1", package = "secp256k1", branch = "patch-0.30.0" } #bitcoin = { git = "https://github.com/ProjectZKM/rust-bitcoin", branch = "patch-0.32.7" } +[features] +imm-wrap-vk = [ + "zkm-zkvm/imm-wrap-vk", + "bitcoin-light-client-circuit/imm-wrap-vk", +] diff --git a/circuits/watchtower-proof/guest/src/main.rs b/circuits/watchtower-proof/guest/src/main.rs index c522794ca..8862f750d 100644 --- a/circuits/watchtower-proof/guest/src/main.rs +++ b/circuits/watchtower-proof/guest/src/main.rs @@ -2,31 +2,24 @@ #![no_main] zkm_zkvm::entrypoint!(main); -use header_chain::{ - HeaderChainCircuitInput, - SPV, -}; -use bitcoin_light_client_circuit::WatchtowerAttestationInputs; use commit_chain::CommitChainCircuitInput; +use header_chain::{HeaderChainCircuitInput, SPV}; use state_chain::StateChainCircuitInput; pub fn main() { let genesis_sequencer_commit_txid = zkm_zkvm::io::read::<[u8; 32]>(); - let latest_sequencer_commit_txid = zkm_zkvm::io::read::<[u8; 32]>(); let header_chain: HeaderChainCircuitInput = zkm_zkvm::io::read(); // private inputs let commit_chain: CommitChainCircuitInput = zkm_zkvm::io::read(); let state_chain: StateChainCircuitInput = zkm_zkvm::io::read(); - let attestation: WatchtowerAttestationInputs = zkm_zkvm::io::read(); let spv: SPV = zkm_zkvm::io::read(); - let output = bitcoin_light_client_circuit::watch_longest_chain( + let (total_work, btc_best_block_height) = bitcoin_light_client_circuit::watch_longest_chain( genesis_sequencer_commit_txid, - latest_sequencer_commit_txid, header_chain, commit_chain, state_chain, - attestation, - spv + spv, ); - zkm_zkvm::io::commit(&output); + zkm_zkvm::io::commit(&total_work); + zkm_zkvm::io::commit(&btc_best_block_height); } diff --git a/circuits/watchtower-proof/host/Cargo.toml b/circuits/watchtower-proof/host/Cargo.toml index 427c24b7a..84306b5b5 100644 --- a/circuits/watchtower-proof/host/Cargo.toml +++ b/circuits/watchtower-proof/host/Cargo.toml @@ -31,6 +31,7 @@ commit-chain = { workspace = true } bitcoin = { workspace = true } client = { workspace = true } proof-builder.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/watchtower-proof/host/src/lib.rs b/circuits/watchtower-proof/host/src/lib.rs index d01589a9f..e4a5341f5 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -1,24 +1,18 @@ #![feature(trim_prefix_suffix)] //! Generate watchtower proof use anyhow::Context; -use bincode::deserialize; use borsh::BorshDeserialize; use commit_chain::{CommitChainCircuitInput, CommitChainPrevProofType}; -use header_chain::{ - BlockHeaderCircuitOutput, CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType, -}; +use header_chain::{CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType}; use zkm_sdk::{ HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, include_elf, }; use bitcoin::{Block, Network, Transaction, Txid, hashes::Hash}; -use bitcoin_light_client_circuit::{ - WatchtowerAttestationInputs, build_spv, load_unique_part_stark_vk_witnesses, - part_stark_vk_attestation_dir, -}; +use bitcoin_light_client_circuit::build_spv; use sha2::{Digest, Sha256}; -use state_chain::{StateChainCircuitInput, StateChainCircuitOutput, StateChainPrevProofType}; +use state_chain::{StateChainCircuitInput, StateChainPrevProofType}; use std::str::FromStr; use std::sync::OnceLock; static ELF_ID: OnceLock = OnceLock::new(); @@ -26,14 +20,15 @@ static ELF_ID: OnceLock = OnceLock::new(); use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; use clap::Parser; -use serde::de::DeserializeOwned; use std::fs; -use std::panic::{AssertUnwindSafe, catch_unwind}; -use zkm_verifier::Groth16Verifier; // The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -43,22 +38,62 @@ pub struct Args { #[arg(long, env, default_value_t = Network::Regtest)] pub bitcoin_network: Network, - #[clap(long, env)] + // Print-only mode skips runtime inputs but keeps them required otherwise. + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub genesis_sequencer_commit_txid: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub latest_sequencer_commit_txid: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'H', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub header_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub commit_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub state_chain_input_proof: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub output: String, } @@ -73,18 +108,32 @@ pub async fn fetch_target_block( latest_sequencer_commit_txid: &str, bitcoin_network: Network, ) -> anyhow::Result<(u32, Block, Transaction)> { - let btc_client = client::btc_chain::BTCClient::new(bitcoin_network, Some(&esplora_url)); - let latest_sequencer_commit_txid = Txid::from_str(latest_sequencer_commit_txid).unwrap(); - - let latest_sequencer_commit_tx = - btc_client.get_tx(&latest_sequencer_commit_txid).await.unwrap().unwrap(); - // TODO: replace it by `get_raw_transaction_info` - let tx_merkle_proof = - btc_client.get_merkle_proof(&latest_sequencer_commit_txid).await.unwrap().unwrap(); - - let block_pos = tx_merkle_proof.block_height; + let btc_client = client::btc_chain::BTCClient::new(bitcoin_network, Some(esplora_url)); + let latest_sequencer_commit_txid = Txid::from_str(latest_sequencer_commit_txid) + .context("invalid latest sequencer commitment txid")?; + let tx_info = btc_client + .get_tx_info(&latest_sequencer_commit_txid) + .await + .with_context(|| { + format!("failed to fetch transaction info for {latest_sequencer_commit_txid}") + })? + .with_context(|| format!("transaction {latest_sequencer_commit_txid} not found"))?; + anyhow::ensure!( + tx_info.status.confirmed, + "transaction {latest_sequencer_commit_txid} is not confirmed" + ); + let block_pos = + tx_info.status.block_height.context("confirmed transaction is missing its block height")?; + let block_hash = + tx_info.status.block_hash.context("confirmed transaction is missing its block hash")?; tracing::info!("block height: {block_pos}"); - let target_block = btc_client.get_block_by_height(block_pos).await.unwrap(); + let target_block = btc_client + .get_block_by_hash(&block_hash) + .await + .with_context(|| format!("failed to fetch block {block_hash}"))? + .with_context(|| format!("block {block_hash} not found"))?; + let latest_sequencer_commit_tx = tx_info.to_tx(); + Ok((block_pos, target_block, latest_sequencer_commit_tx)) } @@ -97,6 +146,7 @@ pub struct WatchtowerProofBuilder { } impl WatchtowerProofBuilder { + #[allow(clippy::new_without_default)] pub fn new() -> Self { let client = ProverClient::new(); let (proving_key, verifying_key) = client.setup(WATCHTOWER); @@ -104,33 +154,6 @@ impl WatchtowerProofBuilder { } } -fn load_proof_public_output(proof_path: &str) -> anyhow::Result { - let public_inputs = fs::read(format!("{proof_path}.public_inputs.bin")) - .context("Failed to read public inputs")?; - deserialize(&public_inputs).context("Failed to decode proof public outputs") -} - -fn load_part_stark_vk(zkm_version: &str) -> anyhow::Result> { - catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec())) - .map_err(|_| anyhow::anyhow!("Failed to load part_stark_vk for zkm_version {zkm_version}")) -} - -/// Collect both the version-derived verifier key and the recursive inner verifier key -/// for header/state subproofs so witness loading survives Ziren upgrades. -fn collect_requested_part_stark_vks( - header_chain_input: &HeaderChainCircuitInput, - header_chain_output: &BlockHeaderCircuitOutput, - state_chain_input: &StateChainCircuitInput, - state_chain_output: &StateChainCircuitOutput, -) -> anyhow::Result>> { - Ok(vec![ - load_part_stark_vk(&header_chain_input.zkm_version)?, - header_chain_output.part_stark_vk.clone(), - load_part_stark_vk(&state_chain_input.zkm_version)?, - state_chain_output.part_stark_vk.clone(), - ]) -} - impl ProofBuilder for WatchtowerProofBuilder { fn client(&self) -> &zkm_sdk::ProverClient { &self.client @@ -157,7 +180,6 @@ impl ProofBuilder for WatchtowerProofBuilder { header_chain_input_proof, commit_chain_input_proof, state_chain_input_proof, - latest_sequencer_commit_txid, genesis_sequencer_commit_txid, target_block, block_pos, @@ -171,12 +193,12 @@ impl ProofBuilder for WatchtowerProofBuilder { // --- header chain --- // let header_chain_input = { let zkm_public_values = - fs::read(&format!("{}.public_inputs.bin", header_chain_input_proof)).unwrap(); + fs::read(format!("{}.public_inputs.bin", header_chain_input_proof)).unwrap(); let zkm_proof = fs::read(header_chain_input_proof) .context("Failed to read input proof file") .unwrap(); let zkm_vk_hash = - fs::read(&format!("{}.vk_hash.bin", header_chain_input_proof)).unwrap(); + fs::read(format!("{}.vk_hash.bin", header_chain_input_proof)).unwrap(); let version_path = format!("{header_chain_input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| format!("failed to read zkm_version file '{version_path}'")) @@ -185,6 +207,8 @@ impl ProofBuilder for WatchtowerProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; HeaderChainCircuitInput { prev_proof: HeaderChainPrevProofType::GenesisBlock, // unused @@ -192,6 +216,7 @@ impl ProofBuilder for WatchtowerProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers: vec![], } }; @@ -199,12 +224,12 @@ impl ProofBuilder for WatchtowerProofBuilder { // --- commit chain --- // let commit_chain_input = { let zkm_public_values = - fs::read(&format!("{}.public_inputs.bin", commit_chain_input_proof)).unwrap(); + fs::read(format!("{}.public_inputs.bin", commit_chain_input_proof)).unwrap(); let zkm_proof = fs::read(commit_chain_input_proof) .context("Failed to read input proof file") .unwrap(); let zkm_vk_hash = - fs::read(&format!("{}.vk_hash.bin", commit_chain_input_proof)).unwrap(); + fs::read(format!("{}.vk_hash.bin", commit_chain_input_proof)).unwrap(); let version_path = format!("{commit_chain_input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| format!("failed to read zkm_version file '{version_path}'")) @@ -213,12 +238,15 @@ impl ProofBuilder for WatchtowerProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; CommitChainCircuitInput { prev_proof: CommitChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, commits: vec![], } }; @@ -229,9 +257,8 @@ impl ProofBuilder for WatchtowerProofBuilder { .context("Failed to read input proof file") .unwrap(); let zkm_public_values = - fs::read(&format!("{}.public_inputs.bin", state_chain_input_proof)).unwrap(); - let zkm_vk_hash = - fs::read(&format!("{}.vk_hash.bin", state_chain_input_proof)).unwrap(); + fs::read(format!("{}.public_inputs.bin", state_chain_input_proof)).unwrap(); + let zkm_vk_hash = fs::read(format!("{}.vk_hash.bin", state_chain_input_proof)).unwrap(); let version_path = format!("{state_chain_input_proof}.zkm_version.bin"); let zkm_version = fs::read(&version_path) .with_context(|| format!("failed to read zkm_version file '{version_path}'")) @@ -240,36 +267,22 @@ impl ProofBuilder for WatchtowerProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; StateChainCircuitInput { prev_proof: StateChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: vec![], } }; - let header_chain_output: BlockHeaderCircuitOutput = - load_proof_public_output(header_chain_input_proof)?; - let state_chain_output: StateChainCircuitOutput = - load_proof_public_output(state_chain_input_proof)?; - let attestation_dir = part_stark_vk_attestation_dir(); - let requested_part_stark_vks = collect_requested_part_stark_vks( - &header_chain_input, - &header_chain_output, - &state_chain_input, - &state_chain_output, - )?; - let (unique_witnesses, _) = - load_unique_part_stark_vk_witnesses(&attestation_dir, &requested_part_stark_vks) - .map_err(anyhow::Error::msg)?; - let attestation_inputs = WatchtowerAttestationInputs { unique_witnesses }; - // --- spv --- // - let genesis_sequencer_commit_txid = Txid::from_str(&genesis_sequencer_commit_txid)?; - let latest_sequencer_commit_txid = Txid::from_str(&latest_sequencer_commit_txid)?; + let genesis_sequencer_commit_txid = Txid::from_str(genesis_sequencer_commit_txid)?; let bitcoin_block_headers = { - let headers: Vec = std::fs::read(&format!("{header_chain_input_proof}.blocks"))?; + let headers: Vec = std::fs::read(format!("{header_chain_input_proof}.blocks"))?; headers .chunks(80) .map(|header| CircuitBlockHeader::try_from_slice(header).unwrap()) @@ -299,11 +312,9 @@ impl ProofBuilder for WatchtowerProofBuilder { || -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { let mut stdin = ZKMStdin::new(); stdin.write(&genesis_sequencer_commit_txid.to_byte_array()); - stdin.write(&latest_sequencer_commit_txid.to_byte_array()); stdin.write(&header_chain_input); stdin.write(&commit_chain_input); stdin.write(&state_chain_input); - stdin.write(&attestation_inputs); stdin.write(&spv); let elf_id = if ELF_ID.get().is_none() { ELF_ID @@ -340,15 +351,13 @@ impl ProofBuilder for WatchtowerProofBuilder { let ProofRequest::WatchtowerProofRequest { output, .. } = ctx else { anyhow::bail!("invalid context"); }; - std::fs::write(&format!("{}", output), proof.bytes())?; + std::fs::write(output, proof.bytes())?; let public_value_hex = hex::encode(proof.public_values.to_vec()); let proof_size = proof.bytes().len(); let zkm_version = proof.zkm_version.clone(); - let proof_part_stark_vk = load_part_stark_vk(&zkm_version)?; - std::fs::write(&format!("{}.public_inputs.bin", output), proof.public_values.to_vec())?; - std::fs::write(&format!("{}.vk_hash.bin", output), self.verifying_key.bytes32())?; - std::fs::write(&format!("{}.zkm_version.bin", output), zkm_version)?; - std::fs::write(&format!("{}.proof_part_stark_vk.bin", output), proof_part_stark_vk)?; + std::fs::write(format!("{}.public_inputs.bin", output), proof.public_values.to_vec())?; + std::fs::write(format!("{}.vk_hash.bin", output), self.verifying_key.bytes32())?; + std::fs::write(format!("{}.zkm_version.bin", output), zkm_version)?; Ok((public_value_hex, proof_size)) } } @@ -357,52 +366,28 @@ impl ProofBuilder for WatchtowerProofBuilder { mod tests { use super::*; - fn sample_header_input(zkm_version: &str) -> HeaderChainCircuitInput { - HeaderChainCircuitInput { - prev_proof: HeaderChainPrevProofType::GenesisBlock, - zkm_proof: vec![], - zkm_public_values: vec![], - zkm_vk_hash: vec![], - zkm_version: zkm_version.to_string(), - block_headers: vec![], - } - } - - fn sample_state_input(zkm_version: &str) -> StateChainCircuitInput { - StateChainCircuitInput { - prev_proof: StateChainPrevProofType::GenesisBlock, - zkm_proof: vec![], - zkm_public_values: vec![], - zkm_vk_hash: vec![], - zkm_version: zkm_version.to_string(), - blocks: vec![], - } - } - - fn sample_header_output(part_stark_vk: Vec) -> BlockHeaderCircuitOutput { - BlockHeaderCircuitOutput { chain_state: header_chain::ChainState::new(), part_stark_vk } - } - - fn sample_state_output(part_stark_vk: Vec) -> StateChainCircuitOutput { - StateChainCircuitOutput { - chain_state: state_chain::StateChainState::new(0, [0u8; 32], Vec::new()), - part_stark_vk, - } - } - - #[test] - fn test_collect_requested_part_stark_vks_includes_outer_and_inner_versions() { - let old_vk = load_part_stark_vk("v1.2.4").unwrap(); - let new_vk = load_part_stark_vk("v1.2.5").unwrap(); - - let requested = collect_requested_part_stark_vks( - &sample_header_input("v1.2.5"), - &sample_header_output(old_vk.clone()), - &sample_state_input("v1.2.5"), - &sample_state_output(old_vk.clone()), + #[tokio::test] + #[ignore = "requires public testnet4 Esplora"] + async fn fetch_target_block_from_testnet4() -> anyhow::Result<()> { + let (block_num, block, tx) = fetch_target_block( + "https://mempool.space/testnet4/api", + "c2d816940391da550af23c7924f8d9ecd276cdf6a22a164fc44443f5eba6d4b5", + Network::Testnet4, ) - .unwrap(); - - assert_eq!(requested, vec![new_vk.clone(), old_vk.clone(), new_vk, old_vk]); + .await?; + // println!("fetch_target_block result: {block_num:#?}, {block:#?}, {tx:#?}"); + + assert_eq!(block_num, 146920); + assert_eq!( + block.block_hash(), + bitcoin::BlockHash::from_str( + "00000000000000024f1da869c78a77e4b88043ca8ed57f76fb79d180578897a8" + )? + ); + assert_eq!( + tx.compute_txid().to_string(), + "c2d816940391da550af23c7924f8d9ecd276cdf6a22a164fc44443f5eba6d4b5" + ); + Ok(()) } } diff --git a/circuits/watchtower-proof/host/src/main.rs b/circuits/watchtower-proof/host/src/main.rs index d5d2a8186..a39701d03 100644 --- a/circuits/watchtower-proof/host/src/main.rs +++ b/circuits/watchtower-proof/host/src/main.rs @@ -10,6 +10,12 @@ async fn main() { let args = Args::parse(); // Setup the logger. zkm_sdk::utils::setup_logger(); + let builder = WatchtowerProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let (block_pos, target_block, latest_sequencer_commit_tx) = fetch_target_block( &args.esplora_url, &args.latest_sequencer_commit_txid, @@ -17,8 +23,6 @@ async fn main() { ) .await .unwrap(); - let builder = WatchtowerProofBuilder::new(); - let ctx = ProofRequest::WatchtowerProofRequest { genesis_sequencer_commit_txid: args.genesis_sequencer_commit_txid.clone(), latest_sequencer_commit_txid: args.latest_sequencer_commit_txid.clone(), diff --git a/crates/bitcoin-light-client-circuit/Cargo.toml b/crates/bitcoin-light-client-circuit/Cargo.toml index 172e55302..24c8383f2 100644 --- a/crates/bitcoin-light-client-circuit/Cargo.toml +++ b/crates/bitcoin-light-client-circuit/Cargo.toml @@ -16,6 +16,7 @@ state-chain = { workspace = true } # Ziren zkm-verifier = { workspace = true } +verifier = { workspace = true } zkm-zkvm = { workspace = true } zkm-primitives = { workspace = true } #zkm-verifier = { path = "../../../Ziren/crates/verifier" } @@ -25,10 +26,9 @@ zkm-primitives = { workspace = true } tracing = { workspace = true, features = ["max_level_trace"] } alloy-primitives = { workspace = true, features = ["sha3-keccak", "map-foldhash", "serde"] } -revm-database-interface = { workspace = true, features = ["serde"]} +revm-database-interface = { workspace = true, features = ["serde"] } #revm = { workspace = true, branch = "patch-31.0.2", features = ["serde", "bn"], default-features = false } -sha2 = "0.10.9" hex = { workspace = true } #prost = { version = "0.13", features = ["prost-derive"], default-features = false } @@ -40,13 +40,21 @@ serde_json = { workspace = true } tendermint = { git = "https://github.com/ProjectZKM/tendermint-rs", branch = "patch-0.40.3", default-features = false, features = ["secp256k1"] } tendermint-light-client-verifier = { git = "https://github.com/ProjectZKM/tendermint-rs", branch = "patch-0.40.3", default-features = false, features = [ - "rust-crypto", + "rust-crypto", ] } +[features] +imm-wrap-vk = [ + "verifier/imm-wrap-vk", + "header-chain/imm-wrap-vk", + "commit-chain/imm-wrap-vk", + "state-chain/imm-wrap-vk", +] + [dev-dependencies] rand = "0.8.5" -borsh = {version = "1.5.3", features = ["derive"] } +borsh = { version = "1.5.3", features = ["derive"] } blake3 = "1.6.1" #zkm-sdk = { git = "https://github.com/ProjectZKM/Ziren" } diff --git a/crates/bitcoin-light-client-circuit/src/attestation.rs b/crates/bitcoin-light-client-circuit/src/attestation.rs index dd4b166b2..e69de29bb 100644 --- a/crates/bitcoin-light-client-circuit/src/attestation.rs +++ b/crates/bitcoin-light-client-circuit/src/attestation.rs @@ -1,1494 +0,0 @@ -use bitcoin::secp256k1::{Message, PublicKey, Secp256k1, SecretKey, ecdsa::Signature}; -use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use std::collections::{BTreeMap, BTreeSet}; -use std::path::{Path, PathBuf}; - -pub const PART_STARK_VK_TREE_HEIGHT: usize = 6; -pub const PART_STARK_VK_TREE_LEAFS: usize = 1 << PART_STARK_VK_TREE_HEIGHT; -pub const PART_STARK_VK_ROOT_SIGNATURE_DOMAIN: &[u8] = b"bitvm2:part_stark_vk_root:v2"; -pub const PART_STARK_VK_PUBLISHER_SET_DOMAIN: &[u8] = b"bitvm2:publisher_set:v1"; -pub const PART_STARK_VK_ATTESTATION_DIR_ENV: &str = "PART_STARK_VK_ATTESTATION_DIR"; -pub const DEFAULT_PART_STARK_VK_ATTESTATION_DIR: &str = "data/psv-attestations"; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct PartStarkVkRootSignature { - pub signer_pubkey_index: usize, - pub signature: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct PartStarkVkAttestationBundle { - pub part_stark_vk: Vec, - pub leaf_index: usize, - pub merkle_path: Vec<[u8; 32]>, - pub root: [u8; 32], - pub threshold: u16, - pub publisher_set_id: [u8; 32], - pub signatures: Vec, -} - -pub type UniquePartStarkVkWitness = PartStarkVkAttestationBundle; - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct WatchtowerAttestationInputs { - pub unique_witnesses: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct OperatorAttestationInputs { - pub unique_witnesses: Vec, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct PartStarkVkTreeState { - pub tree_height: usize, - pub leaves: Vec>, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct LatestPartStarkVkAttestationManifest { - pub tree_height: usize, - pub ordered_versions: Vec, - pub root: [u8; 32], - pub threshold: Option, - pub publisher_set_id: Option<[u8; 32]>, - pub publisher_public_keys: Option>, - pub signatures: Vec, - pub part_stark_vk_leaf_hashes: BTreeMap, -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -pub struct VersionedPartStarkVkMerkleProof { - pub version: String, - pub part_stark_vk: Vec, - pub leaf_index: usize, - pub merkle_path: Vec<[u8; 32]>, -} - -pub fn part_stark_vk_attestation_dir() -> PathBuf { - std::env::var(PART_STARK_VK_ATTESTATION_DIR_ENV) - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from(DEFAULT_PART_STARK_VK_ATTESTATION_DIR)) -} - -/// Hash the ordered publisher set together with the active threshold. -pub fn compute_publisher_set_id(publisher_public_keys: &[PublicKey], threshold: u16) -> [u8; 32] { - let mut bytes = Vec::with_capacity( - PART_STARK_VK_PUBLISHER_SET_DOMAIN.len() + 1 + 2 + 4 + publisher_public_keys.len() * 33, - ); - bytes.extend_from_slice(PART_STARK_VK_PUBLISHER_SET_DOMAIN); - bytes.push(0x00); - bytes.extend_from_slice(&threshold.to_le_bytes()); - bytes.extend_from_slice(&(publisher_public_keys.len() as u32).to_le_bytes()); - for pubkey in publisher_public_keys { - bytes.extend_from_slice(&pubkey.serialize()); - } - - Sha256::digest(bytes).into() -} - -pub fn part_stark_vk_leaf_hash(part_stark_vk: &[u8]) -> [u8; 32] { - let mut bytes = Vec::with_capacity(1 + 4 + part_stark_vk.len()); - bytes.push(0x00); - bytes.extend_from_slice(&(part_stark_vk.len() as u32).to_le_bytes()); - bytes.extend_from_slice(part_stark_vk); - - Sha256::digest(bytes).into() -} - -pub fn part_stark_vk_internal_hash(left: [u8; 32], right: [u8; 32]) -> [u8; 32] { - let mut bytes = Vec::with_capacity(1 + 32 + 32); - bytes.push(0x01); - bytes.extend_from_slice(&left); - bytes.extend_from_slice(&right); - - Sha256::digest(bytes).into() -} - -pub fn empty_part_stark_vk_leaf_hash() -> [u8; 32] { - part_stark_vk_leaf_hash(&[]) -} - -pub fn part_stark_vk_bundle_id(part_stark_vk: &[u8]) -> String { - hex::encode(part_stark_vk_leaf_hash(part_stark_vk)) -} - -pub fn part_stark_vk_tree_state_path(dir: &Path) -> PathBuf { - dir.join("tree_state.json") -} - -pub fn part_stark_vk_bundle_path(dir: &Path, part_stark_vk: &[u8]) -> PathBuf { - dir.join("bundles").join(format!("{}.json", part_stark_vk_bundle_id(part_stark_vk))) -} - -pub fn latest_part_stark_vk_attestation_manifest_path(dir: &Path) -> PathBuf { - dir.join("manifest.json") -} - -pub fn latest_part_stark_vk_attestation_proofs_dir(dir: &Path) -> PathBuf { - dir.join("proofs") -} - -pub fn latest_part_stark_vk_attestation_proof_path(dir: &Path, version: &str) -> PathBuf { - latest_part_stark_vk_attestation_proofs_dir(dir).join(format!("{version}.json")) -} - -fn build_part_stark_vk_leaf_layer( - part_stark_vks: &[Vec], - tree_height: usize, -) -> Result, String> { - let total_leafs = 1usize << tree_height; - if part_stark_vks.len() > total_leafs { - return Err(format!( - "too many part_stark_vk leafs: {}, max {}", - part_stark_vks.len(), - total_leafs - )); - } - - let mut nodes = vec![empty_part_stark_vk_leaf_hash(); total_leafs]; - for (index, part_stark_vk) in part_stark_vks.iter().enumerate() { - nodes[index] = part_stark_vk_leaf_hash(part_stark_vk); - } - Ok(nodes) -} - -pub fn build_part_stark_vk_merkle_root( - part_stark_vks: &[Vec], - tree_height: usize, -) -> Result<[u8; 32], String> { - let mut level = build_part_stark_vk_leaf_layer(part_stark_vks, tree_height)?; - while level.len() > 1 { - level = level - .chunks_exact(2) - .map(|pair| part_stark_vk_internal_hash(pair[0], pair[1])) - .collect(); - } - Ok(level[0]) -} - -pub fn build_part_stark_vk_merkle_path( - part_stark_vks: &[Vec], - tree_height: usize, - leaf_index: usize, -) -> Result, String> { - let total_leafs = 1usize << tree_height; - if leaf_index >= total_leafs { - return Err(format!("leaf index {} out of range {}", leaf_index, total_leafs)); - } - - let mut level = build_part_stark_vk_leaf_layer(part_stark_vks, tree_height)?; - let mut index = leaf_index; - let mut path = Vec::with_capacity(tree_height); - while level.len() > 1 { - path.push(level[index ^ 1]); - level = level - .chunks_exact(2) - .map(|pair| part_stark_vk_internal_hash(pair[0], pair[1])) - .collect(); - index /= 2; - } - Ok(path) -} - -pub fn verify_part_stark_vk_merkle_path( - part_stark_vk: &[u8], - leaf_index: usize, - merkle_path: &[[u8; 32]], - expected_root: [u8; 32], - tree_height: usize, -) -> Result<(), String> { - if merkle_path.len() != tree_height { - return Err(format!( - "invalid merkle path length: {}, expected {}", - merkle_path.len(), - tree_height - )); - } - - let total_leafs = 1usize << tree_height; - if leaf_index >= total_leafs { - return Err(format!("leaf index {} out of range {}", leaf_index, total_leafs)); - } - - let mut node = part_stark_vk_leaf_hash(part_stark_vk); - let mut index = leaf_index; - for sibling in merkle_path { - node = if index.is_multiple_of(2) { - part_stark_vk_internal_hash(node, *sibling) - } else { - part_stark_vk_internal_hash(*sibling, node) - }; - index /= 2; - } - - if node != expected_root { - return Err("part_stark_vk merkle root mismatch".to_string()); - } - Ok(()) -} - -/// Domain-separate the attestation root by tree height, threshold and publisher set identity. -pub fn build_attestation_message_digest( - tree_height: usize, - root: [u8; 32], - threshold: u16, - publisher_set_id: [u8; 32], -) -> [u8; 32] { - let mut bytes = - Vec::with_capacity(PART_STARK_VK_ROOT_SIGNATURE_DOMAIN.len() + 1 + 1 + 1 + 2 + 32 + 32); - bytes.extend_from_slice(PART_STARK_VK_ROOT_SIGNATURE_DOMAIN); - bytes.push(0x00); - bytes.push(tree_height as u8); - bytes.push(0x00); - bytes.extend_from_slice(&threshold.to_le_bytes()); - bytes.extend_from_slice(&publisher_set_id); - bytes.extend_from_slice(&root); - - Sha256::digest(bytes).into() -} - -fn verify_part_stark_vk_root_signatures( - signatures: &[PartStarkVkRootSignature], - publisher_public_keys: &[PublicKey], - threshold: u16, - tree_height: usize, - root: [u8; 32], - publisher_set_id: [u8; 32], -) -> Result<(), String> { - if publisher_public_keys.is_empty() { - return Err("publisher_public_keys is empty".to_string()); - } - let required = usize::from(threshold); - if required == 0 { - return Err("threshold must be greater than 0".to_string()); - } - if required > publisher_public_keys.len() { - return Err(format!( - "threshold {} exceeds publisher_public_keys length {}", - required, - publisher_public_keys.len() - )); - } - let message = Message::from_digest(build_attestation_message_digest( - tree_height, - root, - threshold, - publisher_set_id, - )); - let secp = Secp256k1::verification_only(); - let mut seen = std::collections::BTreeSet::new(); - let mut valid = 0usize; - - for root_signature in signatures { - if root_signature.signer_pubkey_index >= publisher_public_keys.len() { - return Err(format!( - "signer_pubkey_index {} out of range {}", - root_signature.signer_pubkey_index, - publisher_public_keys.len() - )); - } - if !seen.insert(root_signature.signer_pubkey_index) { - continue; - } - - let signature = Signature::from_compact(&root_signature.signature) - .map_err(|err| format!("invalid signature encoding: {err}"))?; - let mut normalized = signature; - normalized.normalize_s(); - if normalized != signature { - return Err("signature is not low-s normalized".to_string()); - } - - secp.verify_ecdsa( - &message, - &signature, - &publisher_public_keys[root_signature.signer_pubkey_index], - ) - .map_err(|err| format!("invalid publisher root signature: {err}"))?; - valid += 1; - } - - if valid < required { - return Err(format!( - "not enough valid publisher signatures: got {}, required {}", - valid, required - )); - } - Ok(()) -} - -/// Verify that a `part_stark_vk` belongs to the fixed-height history tree and that the root is -/// approved by the current commit-chain publisher set. -pub fn verify_part_stark_vk_attestation( - bundle: &PartStarkVkAttestationBundle, - publisher_public_keys: &[PublicKey], - threshold: u16, - tree_height: usize, -) -> Result<(), String> { - if bundle.threshold != threshold { - return Err(format!( - "attestation threshold mismatch: bundle {}, expected {}", - bundle.threshold, threshold - )); - } - let expected_publisher_set_id = compute_publisher_set_id(publisher_public_keys, threshold); - if bundle.publisher_set_id != expected_publisher_set_id { - return Err("attestation publisher_set_id mismatch".to_string()); - } - verify_part_stark_vk_merkle_path( - &bundle.part_stark_vk, - bundle.leaf_index, - &bundle.merkle_path, - bundle.root, - tree_height, - )?; - verify_part_stark_vk_root_signatures( - &bundle.signatures, - publisher_public_keys, - threshold, - tree_height, - bundle.root, - bundle.publisher_set_id, - )?; - - Ok(()) -} - -/// Verify each unique witness once so later lookups can reuse the verified payload. -pub fn verify_unique_part_stark_vk_witnesses( - unique_witnesses: &[UniquePartStarkVkWitness], - publisher_public_keys: &[PublicKey], - threshold: u16, - tree_height: usize, -) -> Result<(), String> { - for witness in unique_witnesses { - verify_part_stark_vk_attestation(witness, publisher_public_keys, threshold, tree_height)?; - } - Ok(()) -} - -/// Check whether a target `part_stark_vk` is present in the verified witness set. -pub fn assert_part_stark_vk_in_verified_witnesses( - unique_witnesses: &[UniquePartStarkVkWitness], - expected_part_stark_vk: &[u8], -) -> Result<(), String> { - if unique_witnesses.iter().any(|witness| witness.part_stark_vk == expected_part_stark_vk) { - return Ok(()); - } - Err("part_stark_vk not found in verified witnesses".to_string()) -} - -pub fn load_part_stark_vk_tree_state(dir: &Path) -> Result { - let path = part_stark_vk_tree_state_path(dir); - let bytes = std::fs::read(&path) - .map_err(|err| format!("failed to read tree state '{}': {err}", path.display()))?; - serde_json::from_slice(&bytes) - .map_err(|err| format!("failed to decode tree state '{}': {err}", path.display())) -} - -fn remove_path_if_exists(path: &Path) -> Result<(), String> { - if !path.exists() { - return Ok(()); - } - if path.is_dir() { - std::fs::remove_dir_all(path) - .map_err(|err| format!("failed to remove dir '{}': {err}", path.display()))?; - } else { - std::fs::remove_file(path) - .map_err(|err| format!("failed to remove file '{}': {err}", path.display()))?; - } - Ok(()) -} - -/// Build the latest snapshot manifest and per-version merkle proofs from ordered versions. -fn build_latest_part_stark_vk_attestation_snapshot( - ordered_versions: &[String], - part_stark_vks: &[Vec], - threshold: Option, - publisher_set_id: Option<[u8; 32]>, - publisher_public_keys: Option>, - signatures: Vec, -) -> Result<(LatestPartStarkVkAttestationManifest, Vec), String> { - if ordered_versions.is_empty() { - return Err("ordered_versions is empty".to_string()); - } - if ordered_versions.len() != part_stark_vks.len() { - return Err(format!( - "ordered_versions length {} does not match part_stark_vks length {}", - ordered_versions.len(), - part_stark_vks.len() - )); - } - - let root = build_part_stark_vk_merkle_root(part_stark_vks, PART_STARK_VK_TREE_HEIGHT)?; - let mut part_stark_vk_leaf_hashes = BTreeMap::new(); - let mut proofs = Vec::with_capacity(ordered_versions.len()); - for (leaf_index, (version, part_stark_vk)) in - ordered_versions.iter().zip(part_stark_vks).enumerate() - { - let leaf_hash = part_stark_vk_bundle_id(part_stark_vk); - if part_stark_vk_leaf_hashes.insert(version.clone(), leaf_hash).is_some() { - return Err(format!("duplicate ordered version '{version}'")); - } - proofs.push(VersionedPartStarkVkMerkleProof { - version: version.clone(), - part_stark_vk: part_stark_vk.clone(), - leaf_index, - merkle_path: build_part_stark_vk_merkle_path( - part_stark_vks, - PART_STARK_VK_TREE_HEIGHT, - leaf_index, - )?, - }); - } - - Ok(( - LatestPartStarkVkAttestationManifest { - tree_height: PART_STARK_VK_TREE_HEIGHT, - ordered_versions: ordered_versions.to_vec(), - root, - threshold, - publisher_set_id, - publisher_public_keys, - signatures, - part_stark_vk_leaf_hashes, - }, - proofs, - )) -} - -/// Rewrite the attestation directory so it only contains the current snapshot files. -fn write_latest_part_stark_vk_attestation_snapshot( - dir: &Path, - manifest: &LatestPartStarkVkAttestationManifest, - proofs: &[VersionedPartStarkVkMerkleProof], -) -> Result<(), String> { - std::fs::create_dir_all(dir) - .map_err(|err| format!("failed to create attestation dir '{}': {err}", dir.display()))?; - remove_path_if_exists(&latest_part_stark_vk_attestation_proofs_dir(dir))?; - remove_path_if_exists(&dir.join("bundles"))?; - remove_path_if_exists(&part_stark_vk_tree_state_path(dir))?; - remove_path_if_exists(&latest_part_stark_vk_attestation_manifest_path(dir))?; - - let proofs_dir = latest_part_stark_vk_attestation_proofs_dir(dir); - std::fs::create_dir_all(&proofs_dir).map_err(|err| { - format!("failed to create attestation proofs dir '{}': {err}", proofs_dir.display()) - })?; - - for proof in proofs { - let path = latest_part_stark_vk_attestation_proof_path(dir, &proof.version); - let bytes = serde_json::to_vec_pretty(proof) - .map_err(|err| format!("failed to encode proof '{}': {err}", path.display()))?; - std::fs::write(&path, bytes) - .map_err(|err| format!("failed to write proof '{}': {err}", path.display()))?; - } - - let manifest_path = latest_part_stark_vk_attestation_manifest_path(dir); - let manifest_bytes = serde_json::to_vec_pretty(manifest).map_err(|err| { - format!("failed to encode latest attestation manifest '{}': {err}", manifest_path.display()) - })?; - std::fs::write(&manifest_path, manifest_bytes).map_err(|err| { - format!("failed to write latest attestation manifest '{}': {err}", manifest_path.display()) - })?; - Ok(()) -} - -/// Persist a full latest snapshot, optionally including publisher metadata and signatures. -pub fn save_latest_part_stark_vk_attestation_snapshot( - dir: &Path, - ordered_versions: &[String], - part_stark_vks: &[Vec], - threshold: Option, - publisher_set_id: Option<[u8; 32]>, - publisher_public_keys: Option>, - signatures: Vec, -) -> Result<(), String> { - let publisher_public_keys = - publisher_public_keys.map(|keys| keys.into_iter().map(|key| key.to_string()).collect()); - let (manifest, proofs) = build_latest_part_stark_vk_attestation_snapshot( - ordered_versions, - part_stark_vks, - threshold, - publisher_set_id, - publisher_public_keys, - signatures, - )?; - write_latest_part_stark_vk_attestation_snapshot(dir, &manifest, &proofs) -} - -/// Load the latest manifest that describes the current attestation snapshot. -pub fn load_latest_part_stark_vk_attestation_manifest( - dir: &Path, -) -> Result { - let path = latest_part_stark_vk_attestation_manifest_path(dir); - let bytes = std::fs::read(&path).map_err(|err| { - format!("failed to read latest attestation manifest '{}': {err}", path.display()) - })?; - serde_json::from_slice(&bytes).map_err(|err| { - format!("failed to decode latest attestation manifest '{}': {err}", path.display()) - }) -} - -/// Load every version proof referenced by the latest manifest in manifest order. -pub fn load_latest_part_stark_vk_attestation_proofs( - dir: &Path, - ordered_versions: &[String], -) -> Result, String> { - let mut proofs = Vec::with_capacity(ordered_versions.len()); - for version in ordered_versions { - let path = latest_part_stark_vk_attestation_proof_path(dir, version); - let bytes = std::fs::read(&path).map_err(|err| { - format!("failed to read attestation proof '{}': {err}", path.display()) - })?; - let proof: VersionedPartStarkVkMerkleProof = - serde_json::from_slice(&bytes).map_err(|err| { - format!("failed to decode attestation proof '{}': {err}", path.display()) - })?; - proofs.push(proof); - } - Ok(proofs) -} - -/// Merge one signer into the latest snapshot, resetting signatures when the signing context changes. -pub fn sign_latest_part_stark_vk_snapshot( - dir: &Path, - ordered_versions: &[String], - part_stark_vks: &[Vec], - publisher_public_keys: &[PublicKey], - threshold: u16, - signer_pubkey_index: usize, - secret_key: &SecretKey, -) -> Result { - if publisher_public_keys.is_empty() { - return Err("publisher_public_keys is empty".to_string()); - } - let required = usize::from(threshold); - if required == 0 { - return Err("threshold must be greater than 0".to_string()); - } - if required > publisher_public_keys.len() { - return Err(format!( - "threshold {} exceeds publisher_public_keys length {}", - required, - publisher_public_keys.len() - )); - } - if signer_pubkey_index >= publisher_public_keys.len() { - return Err(format!( - "signer_pubkey_index {} out of range {}", - signer_pubkey_index, - publisher_public_keys.len() - )); - } - - let signer_public_key = PublicKey::from_secret_key(&Secp256k1::new(), secret_key); - if publisher_public_keys[signer_pubkey_index] != signer_public_key { - return Err(format!( - "publisher_secret_key does not match publisher public key at index {}", - signer_pubkey_index - )); - } - - let root = build_part_stark_vk_merkle_root(part_stark_vks, PART_STARK_VK_TREE_HEIGHT)?; - let publisher_set_id = compute_publisher_set_id(publisher_public_keys, threshold); - let mut signatures = match load_latest_part_stark_vk_attestation_manifest(dir) { - Ok(existing) - if existing.ordered_versions == ordered_versions - && existing.root == root - && existing.publisher_set_id == Some(publisher_set_id) => - { - existing.signatures - } - _ => Vec::new(), - }; - - let signature = PartStarkVkRootSignature { - signer_pubkey_index, - signature: sign_part_stark_vk_root( - secret_key, - PART_STARK_VK_TREE_HEIGHT, - root, - threshold, - publisher_set_id, - ), - }; - - if let Some(existing_signature) = - signatures.iter_mut().find(|existing| existing.signer_pubkey_index == signer_pubkey_index) - { - *existing_signature = signature; - } else { - signatures.push(signature); - } - signatures.sort_by_key(|existing| existing.signer_pubkey_index); - - save_latest_part_stark_vk_attestation_snapshot( - dir, - ordered_versions, - part_stark_vks, - Some(threshold), - Some(publisher_set_id), - Some(publisher_public_keys.to_vec()), - signatures, - )?; - load_latest_part_stark_vk_attestation_manifest(dir) -} - -pub fn save_part_stark_vk_tree_state( - dir: &Path, - state: &PartStarkVkTreeState, -) -> Result<(), String> { - std::fs::create_dir_all(dir) - .map_err(|err| format!("failed to create attestation dir '{}': {err}", dir.display()))?; - let path = part_stark_vk_tree_state_path(dir); - let bytes = serde_json::to_vec_pretty(state) - .map_err(|err| format!("failed to encode tree state '{}': {err}", path.display()))?; - std::fs::write(&path, bytes) - .map_err(|err| format!("failed to write tree state '{}': {err}", path.display())) -} - -pub fn load_part_stark_vk_attestation_bundle( - dir: &Path, - part_stark_vk: &[u8], -) -> Result { - let path = part_stark_vk_bundle_path(dir, part_stark_vk); - let bytes = std::fs::read(&path) - .map_err(|err| format!("failed to read attestation bundle '{}': {err}", path.display()))?; - serde_json::from_slice(&bytes) - .map_err(|err| format!("failed to decode attestation bundle '{}': {err}", path.display())) -} - -pub fn save_part_stark_vk_attestation_bundle( - dir: &Path, - bundle: &PartStarkVkAttestationBundle, -) -> Result<(), String> { - let bundle_dir = dir.join("bundles"); - std::fs::create_dir_all(&bundle_dir).map_err(|err| { - format!("failed to create attestation bundle dir '{}': {err}", bundle_dir.display()) - })?; - let path = part_stark_vk_bundle_path(dir, &bundle.part_stark_vk); - let bytes = serde_json::to_vec_pretty(bundle).map_err(|err| { - format!("failed to encode attestation bundle '{}': {err}", path.display()) - })?; - std::fs::write(&path, bytes) - .map_err(|err| format!("failed to write attestation bundle '{}': {err}", path.display())) -} - -pub fn sign_part_stark_vk_root( - secret_key: &SecretKey, - tree_height: usize, - root: [u8; 32], - threshold: u16, - publisher_set_id: [u8; 32], -) -> Vec { - let secp = Secp256k1::new(); - let message = Message::from_digest(build_attestation_message_digest( - tree_height, - root, - threshold, - publisher_set_id, - )); - let signature = secp.sign_ecdsa(&message, secret_key); - signature.serialize_compact().to_vec() -} - -pub fn build_part_stark_vk_root_signatures( - signer_secret_keys: &[(usize, &SecretKey)], - tree_height: usize, - root: [u8; 32], - threshold: u16, - publisher_set_id: [u8; 32], -) -> Result, String> { - if signer_secret_keys.is_empty() { - return Err("publisher_secret_keys is empty".to_string()); - } - - let mut seen = BTreeSet::new(); - let mut signatures = Vec::with_capacity(signer_secret_keys.len()); - for (signer_pubkey_index, secret_key) in signer_secret_keys { - if !seen.insert(*signer_pubkey_index) { - return Err(format!( - "duplicate signer_pubkey_index {} in publisher_secret_keys", - signer_pubkey_index - )); - } - signatures.push(PartStarkVkRootSignature { - signer_pubkey_index: *signer_pubkey_index, - signature: sign_part_stark_vk_root( - secret_key, - tree_height, - root, - threshold, - publisher_set_id, - ), - }); - } - - Ok(signatures) -} - -pub fn build_part_stark_vk_attestation_bundle( - leaves: &[Vec], - tree_height: usize, - leaf_index: usize, - threshold: u16, - publisher_set_id: [u8; 32], - signatures: Vec, -) -> Result { - let part_stark_vk = leaves - .get(leaf_index) - .ok_or_else(|| format!("leaf index {} out of range {}", leaf_index, leaves.len()))? - .clone(); - Ok(PartStarkVkAttestationBundle { - part_stark_vk, - leaf_index, - merkle_path: build_part_stark_vk_merkle_path(leaves, tree_height, leaf_index)?, - root: build_part_stark_vk_merkle_root(leaves, tree_height)?, - threshold, - publisher_set_id, - signatures, - }) -} - -pub fn append_part_stark_vk_and_sign( - dir: &Path, - part_stark_vk: Vec, - publisher_public_keys: &[PublicKey], - threshold: u16, - publisher_secret_keys: &[SecretKey], -) -> Result { - let indexed_secret_keys = - publisher_secret_keys.iter().enumerate().collect::>(); - append_part_stark_vk_and_sign_with_signers( - dir, - part_stark_vk, - publisher_public_keys, - threshold, - &indexed_secret_keys, - ) -} - -pub fn append_part_stark_vk_and_sign_with_signers( - dir: &Path, - part_stark_vk: Vec, - publisher_public_keys: &[PublicKey], - threshold: u16, - publisher_secret_keys: &[(usize, &SecretKey)], -) -> Result { - let mut state = if part_stark_vk_tree_state_path(dir).exists() { - load_part_stark_vk_tree_state(dir)? - } else { - PartStarkVkTreeState { tree_height: PART_STARK_VK_TREE_HEIGHT, leaves: vec![] } - }; - if state.tree_height != PART_STARK_VK_TREE_HEIGHT { - return Err(format!( - "unsupported tree height {}, expected {}", - state.tree_height, PART_STARK_VK_TREE_HEIGHT - )); - } - if state.leaves.len() >= (1usize << state.tree_height) { - return Err("part_stark_vk history tree is full".to_string()); - } - if state.leaves.iter().any(|leaf| leaf == &part_stark_vk) { - return Err("part_stark_vk already exists in tree".to_string()); - } - state.leaves.push(part_stark_vk.clone()); - let bundle = resign_part_stark_vk_tree( - dir, - &state, - publisher_public_keys, - threshold, - publisher_secret_keys, - state.leaves.len() - 1, - )?; - save_part_stark_vk_tree_state(dir, &state)?; - Ok(bundle) -} - -pub fn resign_current_part_stark_vk_root( - dir: &Path, - publisher_public_keys: &[PublicKey], - threshold: u16, - publisher_secret_keys: &[SecretKey], -) -> Result<(), String> { - let indexed_secret_keys = - publisher_secret_keys.iter().enumerate().collect::>(); - resign_current_part_stark_vk_root_with_signers( - dir, - publisher_public_keys, - threshold, - &indexed_secret_keys, - ) -} - -pub fn resign_current_part_stark_vk_root_with_signers( - dir: &Path, - publisher_public_keys: &[PublicKey], - threshold: u16, - publisher_secret_keys: &[(usize, &SecretKey)], -) -> Result<(), String> { - let state = load_part_stark_vk_tree_state(dir)?; - if state.leaves.is_empty() { - return Err("part_stark_vk tree is empty".to_string()); - } - for leaf_index in 0..state.leaves.len() { - let _ = resign_part_stark_vk_tree( - dir, - &state, - publisher_public_keys, - threshold, - publisher_secret_keys, - leaf_index, - )?; - } - Ok(()) -} - -fn resign_part_stark_vk_tree( - dir: &Path, - state: &PartStarkVkTreeState, - publisher_public_keys: &[PublicKey], - threshold: u16, - publisher_secret_keys: &[(usize, &SecretKey)], - leaf_index: usize, -) -> Result { - let root = build_part_stark_vk_merkle_root(&state.leaves, state.tree_height)?; - let publisher_set_id = compute_publisher_set_id(publisher_public_keys, threshold); - let signatures = build_part_stark_vk_root_signatures( - publisher_secret_keys, - state.tree_height, - root, - threshold, - publisher_set_id, - )?; - let bundle = build_part_stark_vk_attestation_bundle( - &state.leaves, - state.tree_height, - leaf_index, - threshold, - publisher_set_id, - signatures, - )?; - save_part_stark_vk_attestation_bundle(dir, &bundle)?; - Ok(bundle) -} - -pub fn load_unique_part_stark_vk_witnesses( - dir: &Path, - part_stark_vks: &[Vec], -) -> Result<(Vec, Vec), String> { - if !latest_part_stark_vk_attestation_manifest_path(dir).exists() { - return Err(format!( - "missing latest attestation manifest '{}'", - latest_part_stark_vk_attestation_manifest_path(dir).display() - )); - } - - load_unique_part_stark_vk_witnesses_from_latest_snapshot(dir, part_stark_vks) -} - -fn load_unique_part_stark_vk_witnesses_from_latest_snapshot( - dir: &Path, - part_stark_vks: &[Vec], -) -> Result<(Vec, Vec), String> { - let manifest = load_latest_part_stark_vk_attestation_manifest(dir)?; - let threshold = manifest - .threshold - .ok_or_else(|| "latest attestation manifest is missing threshold".to_string())?; - let publisher_set_id = manifest - .publisher_set_id - .ok_or_else(|| "latest attestation manifest is missing publisher_set_id".to_string())?; - let proofs = load_latest_part_stark_vk_attestation_proofs(dir, &manifest.ordered_versions)?; - let mut available_witnesses = BTreeMap::, UniquePartStarkVkWitness>::new(); - - for proof in proofs { - let expected_leaf_hash = - manifest.part_stark_vk_leaf_hashes.get(&proof.version).ok_or_else(|| { - format!("missing part_stark_vk leaf hash for version '{}'", proof.version) - })?; - let actual_leaf_hash = part_stark_vk_bundle_id(&proof.part_stark_vk); - if *expected_leaf_hash != actual_leaf_hash { - return Err(format!( - "part_stark_vk leaf hash mismatch for version '{}': expected {}, got {}", - proof.version, expected_leaf_hash, actual_leaf_hash - )); - } - - verify_part_stark_vk_merkle_path( - &proof.part_stark_vk, - proof.leaf_index, - &proof.merkle_path, - manifest.root, - manifest.tree_height, - )?; - available_witnesses.entry(proof.part_stark_vk.clone()).or_insert_with(|| { - PartStarkVkAttestationBundle { - part_stark_vk: proof.part_stark_vk, - leaf_index: proof.leaf_index, - merkle_path: proof.merkle_path, - root: manifest.root, - threshold, - publisher_set_id, - signatures: manifest.signatures.clone(), - } - }); - } - - let mut unique_witnesses = Vec::new(); - let mut witness_indexes = Vec::with_capacity(part_stark_vks.len()); - let mut seen = BTreeMap::, usize>::new(); - - for part_stark_vk in part_stark_vks { - if let Some(index) = seen.get(part_stark_vk) { - witness_indexes.push(*index); - continue; - } - let bundle = available_witnesses - .get(part_stark_vk) - .cloned() - .ok_or_else(|| "part_stark_vk not found in latest snapshot".to_string())?; - let index = unique_witnesses.len(); - unique_witnesses.push(bundle); - seen.insert(part_stark_vk.clone(), index); - witness_indexes.push(index); - } - - Ok((unique_witnesses, witness_indexes)) -} - -#[cfg(test)] -mod tests { - use bitcoin::secp256k1::{Message, Secp256k1, SecretKey, ecdsa::Signature}; - use std::time::{SystemTime, UNIX_EPOCH}; - - use super::{ - PART_STARK_VK_TREE_HEIGHT, PartStarkVkAttestationBundle, PartStarkVkRootSignature, - assert_part_stark_vk_in_verified_witnesses, build_attestation_message_digest, - build_part_stark_vk_merkle_path, build_part_stark_vk_merkle_root, - build_part_stark_vk_root_signatures, compute_publisher_set_id, - load_latest_part_stark_vk_attestation_manifest, load_unique_part_stark_vk_witnesses, - part_stark_vk_leaf_hash, save_latest_part_stark_vk_attestation_snapshot, - save_part_stark_vk_attestation_bundle, sign_latest_part_stark_vk_snapshot, - verify_part_stark_vk_attestation, verify_unique_part_stark_vk_witnesses, - }; - - fn sample_part_stark_vk() -> Vec { - hex::decode("2000000000000000d157a916a6350249c6e4efd850dcdac3abf5489d36de2d1aa233c9253522871000000000") - .unwrap() - } - - fn sample_part_stark_vk_alt() -> Vec { - b"alternate-part-stark-vk".to_vec() - } - - fn sample_publishers() -> (Vec, Vec) { - let secp = Secp256k1::new(); - let secret_keys = vec![ - SecretKey::from_slice(&[1u8; 32]).unwrap(), - SecretKey::from_slice(&[2u8; 32]).unwrap(), - SecretKey::from_slice(&[3u8; 32]).unwrap(), - SecretKey::from_slice(&[4u8; 32]).unwrap(), - ]; - let publisher_public_keys = secret_keys - .iter() - .map(|sk| bitcoin::secp256k1::PublicKey::from_secret_key(&secp, sk)) - .collect::>(); - (secret_keys, publisher_public_keys) - } - - fn sample_attestation_bundle( - leaves: &[Vec], - leaf_index: usize, - threshold: u16, - publisher_public_keys: &[bitcoin::secp256k1::PublicKey], - signer_secret_keys: &[SecretKey], - ) -> PartStarkVkAttestationBundle { - let root = build_part_stark_vk_merkle_root(leaves, 6).unwrap(); - let publisher_set_id = compute_publisher_set_id(publisher_public_keys, threshold); - let indexed_secret_keys = - signer_secret_keys.iter().enumerate().collect::>(); - let signatures = build_part_stark_vk_root_signatures( - &indexed_secret_keys[..usize::from(threshold)], - 6, - root, - threshold, - publisher_set_id, - ) - .unwrap(); - - PartStarkVkAttestationBundle { - part_stark_vk: leaves[leaf_index].clone(), - leaf_index, - merkle_path: build_part_stark_vk_merkle_path(leaves, 6, leaf_index).unwrap(), - root, - threshold, - publisher_set_id, - signatures, - } - } - - fn unique_test_dir(prefix: &str) -> std::path::PathBuf { - let nanos = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos(); - std::env::temp_dir().join(format!("bitvm2-{prefix}-{nanos}")) - } - - #[test] - fn test_part_stark_vk_leaf_hash_matches_spec() { - assert_eq!( - hex::encode(part_stark_vk_leaf_hash(&[])), - "8855508aade16ec573d21e6a485dfd0a7624085c1a14b5ecdd6485de0c6839a4" - ); - } - - #[test] - fn test_build_part_stark_vk_merkle_root_matches_spec() { - let leaves = vec![sample_part_stark_vk(), b"hello".to_vec()]; - - assert_eq!( - hex::encode(build_part_stark_vk_merkle_root(&leaves, 6).unwrap()), - "8a091f11cab951f0c1228a891c902475b84709e568885a45658840e1b88599d4" - ); - } - - #[test] - fn test_build_attestation_message_digest_matches_spec() { - let secp = Secp256k1::new(); - let secret_keys = [ - SecretKey::from_slice(&[1u8; 32]).unwrap(), - SecretKey::from_slice(&[2u8; 32]).unwrap(), - SecretKey::from_slice(&[3u8; 32]).unwrap(), - SecretKey::from_slice(&[4u8; 32]).unwrap(), - ]; - let publisher_public_keys = secret_keys - .iter() - .map(|sk| bitcoin::secp256k1::PublicKey::from_secret_key(&secp, sk)) - .collect::>(); - let root = hex::decode("8a091f11cab951f0c1228a891c902475b84709e568885a45658840e1b88599d4") - .unwrap() - .try_into() - .unwrap(); - let threshold = 3u16; - let publisher_set_id = compute_publisher_set_id(&publisher_public_keys, threshold); - let mut reversed_public_keys = publisher_public_keys.clone(); - reversed_public_keys.reverse(); - - assert_ne!( - build_attestation_message_digest(6, root, threshold, publisher_set_id), - build_attestation_message_digest(6, root, threshold - 1, publisher_set_id) - ); - assert_ne!(publisher_set_id, compute_publisher_set_id(&reversed_public_keys, threshold)); - } - - #[test] - fn test_verify_part_stark_vk_attestation_accepts_quorum_and_ignores_duplicate_signers() { - let secp = Secp256k1::new(); - let secret_keys = [ - SecretKey::from_slice(&[1u8; 32]).unwrap(), - SecretKey::from_slice(&[2u8; 32]).unwrap(), - SecretKey::from_slice(&[3u8; 32]).unwrap(), - SecretKey::from_slice(&[4u8; 32]).unwrap(), - ]; - let publisher_public_keys = secret_keys - .iter() - .map(|sk| bitcoin::secp256k1::PublicKey::from_secret_key(&secp, sk)) - .collect::>(); - - let part_stark_vk = sample_part_stark_vk(); - let leaves = vec![part_stark_vk.clone()]; - let root = build_part_stark_vk_merkle_root(&leaves, 6).unwrap(); - let threshold = 3u16; - let publisher_set_id = compute_publisher_set_id(&publisher_public_keys, threshold); - let digest = build_attestation_message_digest(6, root, threshold, publisher_set_id); - let message = Message::from_digest(digest); - - let sig0 = secp.sign_ecdsa(&message, &secret_keys[0]); - let sig1 = secp.sign_ecdsa(&message, &secret_keys[1]); - let sig2 = secp.sign_ecdsa(&message, &secret_keys[2]); - - let bundle = PartStarkVkAttestationBundle { - part_stark_vk, - leaf_index: 0, - merkle_path: build_part_stark_vk_merkle_path(&leaves, 6, 0).unwrap(), - root, - threshold, - publisher_set_id, - signatures: vec![ - PartStarkVkRootSignature { - signer_pubkey_index: 0, - signature: sig0.serialize_compact().to_vec(), - }, - PartStarkVkRootSignature { - signer_pubkey_index: 1, - signature: sig1.serialize_compact().to_vec(), - }, - PartStarkVkRootSignature { - signer_pubkey_index: 1, - signature: sig1.serialize_compact().to_vec(), - }, - PartStarkVkRootSignature { - signer_pubkey_index: 2, - signature: sig2.serialize_compact().to_vec(), - }, - ], - }; - - assert!( - verify_part_stark_vk_attestation(&bundle, &publisher_public_keys, threshold, 6).is_ok() - ); - } - - #[test] - fn test_verify_part_stark_vk_attestation_rejects_non_member_signer() { - let secp = Secp256k1::new(); - let secret_keys = [ - SecretKey::from_slice(&[1u8; 32]).unwrap(), - SecretKey::from_slice(&[2u8; 32]).unwrap(), - SecretKey::from_slice(&[3u8; 32]).unwrap(), - ]; - let publisher_public_keys = secret_keys - .iter() - .map(|sk| bitcoin::secp256k1::PublicKey::from_secret_key(&secp, sk)) - .collect::>(); - - let outsider = SecretKey::from_slice(&[9u8; 32]).unwrap(); - let part_stark_vk = sample_part_stark_vk(); - let leaves = vec![part_stark_vk.clone()]; - let root = build_part_stark_vk_merkle_root(&leaves, 6).unwrap(); - let threshold = 2u16; - let publisher_set_id = compute_publisher_set_id(&publisher_public_keys, threshold); - let digest = build_attestation_message_digest(6, root, threshold, publisher_set_id); - let message = Message::from_digest(digest); - let outsider_sig: Signature = secp.sign_ecdsa(&message, &outsider); - - let bundle = PartStarkVkAttestationBundle { - part_stark_vk, - leaf_index: 0, - merkle_path: build_part_stark_vk_merkle_path(&leaves, 6, 0).unwrap(), - root, - threshold, - publisher_set_id, - signatures: vec![PartStarkVkRootSignature { - signer_pubkey_index: 0, - signature: outsider_sig.serialize_compact().to_vec(), - }], - }; - - assert!( - verify_part_stark_vk_attestation(&bundle, &publisher_public_keys, threshold, 6) - .is_err() - ); - } - - #[test] - fn test_build_part_stark_vk_root_signatures_supports_sparse_signer_indexes() { - let secp = Secp256k1::new(); - let secret_keys = [ - SecretKey::from_slice(&[1u8; 32]).unwrap(), - SecretKey::from_slice(&[2u8; 32]).unwrap(), - SecretKey::from_slice(&[3u8; 32]).unwrap(), - SecretKey::from_slice(&[4u8; 32]).unwrap(), - SecretKey::from_slice(&[5u8; 32]).unwrap(), - ]; - let publisher_public_keys = secret_keys - .iter() - .map(|sk| bitcoin::secp256k1::PublicKey::from_secret_key(&secp, sk)) - .collect::>(); - - let leaves = vec![sample_part_stark_vk()]; - let root = build_part_stark_vk_merkle_root(&leaves, 6).unwrap(); - let threshold = 4u16; - let publisher_set_id = compute_publisher_set_id(&publisher_public_keys, threshold); - let signatures = build_part_stark_vk_root_signatures( - &[ - (0, &secret_keys[0]), - (2, &secret_keys[2]), - (3, &secret_keys[3]), - (4, &secret_keys[4]), - ], - 6, - root, - threshold, - publisher_set_id, - ) - .unwrap(); - - let bundle = PartStarkVkAttestationBundle { - part_stark_vk: leaves[0].clone(), - leaf_index: 0, - merkle_path: build_part_stark_vk_merkle_path(&leaves, 6, 0).unwrap(), - root, - threshold, - publisher_set_id, - signatures, - }; - - assert!( - verify_part_stark_vk_attestation(&bundle, &publisher_public_keys, threshold, 6).is_ok() - ); - } - - #[test] - fn test_verify_part_stark_vk_attestation_rejects_threshold_mismatch() { - let secp = Secp256k1::new(); - let secret_keys = [ - SecretKey::from_slice(&[1u8; 32]).unwrap(), - SecretKey::from_slice(&[2u8; 32]).unwrap(), - SecretKey::from_slice(&[3u8; 32]).unwrap(), - SecretKey::from_slice(&[4u8; 32]).unwrap(), - ]; - let publisher_public_keys = secret_keys - .iter() - .map(|sk| bitcoin::secp256k1::PublicKey::from_secret_key(&secp, sk)) - .collect::>(); - let leaves = vec![sample_part_stark_vk()]; - let root = build_part_stark_vk_merkle_root(&leaves, 6).unwrap(); - let signed_threshold = 3u16; - let publisher_set_id = compute_publisher_set_id(&publisher_public_keys, signed_threshold); - let signatures = build_part_stark_vk_root_signatures( - &[(0, &secret_keys[0]), (1, &secret_keys[1]), (2, &secret_keys[2])], - 6, - root, - signed_threshold, - publisher_set_id, - ) - .unwrap(); - - let bundle = PartStarkVkAttestationBundle { - part_stark_vk: leaves[0].clone(), - leaf_index: 0, - merkle_path: build_part_stark_vk_merkle_path(&leaves, 6, 0).unwrap(), - root, - threshold: signed_threshold, - publisher_set_id, - signatures, - }; - - assert!( - verify_part_stark_vk_attestation( - &bundle, - &publisher_public_keys, - signed_threshold - 1, - 6 - ) - .is_err() - ); - } - - #[test] - fn test_verify_unique_part_stark_vk_witnesses_accepts_multiple_bundles() { - let (secret_keys, publisher_public_keys) = sample_publishers(); - let leaves = vec![sample_part_stark_vk(), sample_part_stark_vk_alt()]; - let unique_witnesses = vec![ - sample_attestation_bundle(&leaves, 0, 3, &publisher_public_keys, &secret_keys), - sample_attestation_bundle(&leaves, 1, 3, &publisher_public_keys, &secret_keys), - ]; - - assert!( - verify_unique_part_stark_vk_witnesses(&unique_witnesses, &publisher_public_keys, 3, 6) - .is_ok() - ); - } - - #[test] - fn test_assert_part_stark_vk_in_verified_witnesses_checks_membership() { - let (secret_keys, publisher_public_keys) = sample_publishers(); - let leaves = vec![sample_part_stark_vk(), sample_part_stark_vk_alt()]; - let unique_witnesses = vec![ - sample_attestation_bundle(&leaves, 0, 3, &publisher_public_keys, &secret_keys), - sample_attestation_bundle(&leaves, 1, 3, &publisher_public_keys, &secret_keys), - ]; - - verify_unique_part_stark_vk_witnesses(&unique_witnesses, &publisher_public_keys, 3, 6) - .unwrap(); - assert!(assert_part_stark_vk_in_verified_witnesses(&unique_witnesses, &leaves[1]).is_ok()); - assert!( - assert_part_stark_vk_in_verified_witnesses(&unique_witnesses, b"missing-part-stark-vk") - .is_err() - ); - } - - #[test] - fn test_load_unique_part_stark_vk_witnesses_reuses_duplicate_indexes_from_latest_snapshot() { - let (secret_keys, publisher_public_keys) = sample_publishers(); - let leaves = vec![sample_part_stark_vk(), sample_part_stark_vk_alt()]; - let dir = unique_test_dir("attestation-duplicate-indexes"); - let ordered_versions = vec!["v1.2.4".to_string(), "v1.2.5".to_string()]; - let threshold = 3; - let publisher_set_id = compute_publisher_set_id(&publisher_public_keys, threshold); - let signatures = build_part_stark_vk_root_signatures( - &[(0, &secret_keys[0]), (1, &secret_keys[1]), (2, &secret_keys[2])], - PART_STARK_VK_TREE_HEIGHT, - build_part_stark_vk_merkle_root(&leaves, PART_STARK_VK_TREE_HEIGHT).unwrap(), - threshold, - publisher_set_id, - ) - .unwrap(); - - save_latest_part_stark_vk_attestation_snapshot( - &dir, - &ordered_versions, - &leaves, - Some(threshold), - Some(publisher_set_id), - Some(publisher_public_keys.clone()), - signatures, - ) - .unwrap(); - - let requested = vec![leaves[0].clone(), leaves[1].clone(), leaves[0].clone()]; - let (unique_witnesses, witness_indexes) = - load_unique_part_stark_vk_witnesses(&dir, &requested).unwrap(); - - assert_eq!(unique_witnesses.len(), 2); - assert_eq!(witness_indexes, vec![0, 1, 0]); - - std::fs::remove_dir_all(dir).unwrap(); - } - - #[test] - fn test_load_unique_part_stark_vk_witnesses_requires_latest_manifest() { - let (secret_keys, publisher_public_keys) = sample_publishers(); - let leaves = vec![sample_part_stark_vk(), sample_part_stark_vk_alt()]; - let dir = unique_test_dir("attestation-requires-latest-manifest"); - std::fs::create_dir_all(dir.join("bundles")).unwrap(); - - let first_bundle = - sample_attestation_bundle(&leaves, 0, 3, &publisher_public_keys, &secret_keys); - save_part_stark_vk_attestation_bundle(&dir, &first_bundle).unwrap(); - - let err = load_unique_part_stark_vk_witnesses(&dir, &[leaves[0].clone()]).unwrap_err(); - assert!(err.contains("missing latest attestation manifest")); - - std::fs::remove_dir_all(dir).unwrap(); - } - - #[test] - fn test_save_and_load_latest_snapshot_reuses_duplicate_indexes() { - let (secret_keys, publisher_public_keys) = sample_publishers(); - let leaves = vec![sample_part_stark_vk(), sample_part_stark_vk_alt()]; - let ordered_versions = vec!["v1.2.4".to_string(), "v1.2.5".to_string()]; - let dir = unique_test_dir("latest-attestation-snapshot"); - let threshold = 3; - let publisher_set_id = compute_publisher_set_id(&publisher_public_keys, threshold); - let signatures = build_part_stark_vk_root_signatures( - &[(0, &secret_keys[0]), (1, &secret_keys[1]), (2, &secret_keys[2])], - PART_STARK_VK_TREE_HEIGHT, - build_part_stark_vk_merkle_root(&leaves, PART_STARK_VK_TREE_HEIGHT).unwrap(), - threshold, - publisher_set_id, - ) - .unwrap(); - - save_latest_part_stark_vk_attestation_snapshot( - &dir, - &ordered_versions, - &leaves, - Some(threshold), - Some(publisher_set_id), - Some(publisher_public_keys.clone()), - signatures, - ) - .unwrap(); - - let requested = vec![leaves[0].clone(), leaves[1].clone(), leaves[0].clone()]; - let (unique_witnesses, witness_indexes) = - load_unique_part_stark_vk_witnesses(&dir, &requested).unwrap(); - - assert_eq!(unique_witnesses.len(), 2); - assert_eq!(witness_indexes, vec![0, 1, 0]); - assert_eq!(unique_witnesses[0].part_stark_vk, leaves[0]); - assert_eq!(unique_witnesses[1].part_stark_vk, leaves[1]); - - std::fs::remove_dir_all(dir).unwrap(); - } - - #[test] - fn test_sign_latest_snapshot_preserves_existing_signatures_for_same_identity() { - let (secret_keys, publisher_public_keys) = sample_publishers(); - let leaves = vec![sample_part_stark_vk(), sample_part_stark_vk_alt()]; - let ordered_versions = vec!["v1.2.4".to_string(), "v1.2.5".to_string()]; - let dir = unique_test_dir("latest-attestation-signatures"); - - sign_latest_part_stark_vk_snapshot( - &dir, - &ordered_versions, - &leaves, - &publisher_public_keys, - 3, - 0, - &secret_keys[0], - ) - .unwrap(); - let first_manifest = load_latest_part_stark_vk_attestation_manifest(&dir).unwrap(); - assert_eq!(first_manifest.signatures.len(), 1); - - sign_latest_part_stark_vk_snapshot( - &dir, - &ordered_versions, - &leaves, - &publisher_public_keys, - 3, - 2, - &secret_keys[2], - ) - .unwrap(); - let second_manifest = load_latest_part_stark_vk_attestation_manifest(&dir).unwrap(); - assert_eq!(second_manifest.signatures.len(), 2); - - std::fs::remove_dir_all(dir).unwrap(); - } - - #[test] - fn test_sign_latest_snapshot_clears_stale_signatures_when_publisher_set_changes() { - let (secret_keys, publisher_public_keys) = sample_publishers(); - let leaves = vec![sample_part_stark_vk(), sample_part_stark_vk_alt()]; - let ordered_versions = vec!["v1.2.4".to_string(), "v1.2.5".to_string()]; - let dir = unique_test_dir("latest-attestation-publisher-reset"); - - sign_latest_part_stark_vk_snapshot( - &dir, - &ordered_versions, - &leaves, - &publisher_public_keys, - 3, - 0, - &secret_keys[0], - ) - .unwrap(); - sign_latest_part_stark_vk_snapshot( - &dir, - &ordered_versions, - &leaves, - &publisher_public_keys, - 3, - 1, - &secret_keys[1], - ) - .unwrap(); - - let reordered_publisher_public_keys = vec![ - publisher_public_keys[1], - publisher_public_keys[0], - publisher_public_keys[2], - publisher_public_keys[3], - ]; - sign_latest_part_stark_vk_snapshot( - &dir, - &ordered_versions, - &leaves, - &reordered_publisher_public_keys, - 3, - 0, - &secret_keys[1], - ) - .unwrap(); - - let manifest = load_latest_part_stark_vk_attestation_manifest(&dir).unwrap(); - assert_eq!(manifest.signatures.len(), 1); - assert_eq!(manifest.signatures[0].signer_pubkey_index, 0); - - std::fs::remove_dir_all(dir).unwrap(); - } -} diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index bd3560d31..7f58c96f3 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -1,38 +1,33 @@ -mod attestation; mod signature; mod utils; use alloy_primitives::U32; -pub use attestation::*; pub use signature::*; -use state_chain::verify_sequencer_commit; pub use utils::*; use alloy_primitives::U256; use bitcoin::Block; -use bitcoin::Transaction; use bitcoin::hashes::{Hash, HashEngine, sha256}; -use commit_chain::sequencer_hash; use commit_chain::{ - CommitChainCircuitInput, CommitChainCircuitOutput, extract_data_from_commitment_outputs, + AuthorizedProgramIds, CommitChainCircuitInput, CommitChainCircuitOutput, + commit_chain_commitment_digest, decode_commit_chain_circuit_output, + extract_commit_chain_commitment, extract_data_from_commitment_outputs, sequencer_hash, }; use header_chain::{ - BitcoinMerkleTree, CircuitBlockHeader, CircuitTransaction, HeaderChainCircuitInput, - HeaderChainPrevProofType, MMRHost, SPV, verify_merkle_proof, + BitcoinMerkleTree, BlockHeaderCircuitOutput, CircuitBlockHeader, CircuitTransaction, + HeaderChainCircuitInput, MMRHost, SPV, verify_merkle_proof, }; -use state_chain::{StateChainCircuitInput, StateChainPrevProofType}; -use std::panic::{AssertUnwindSafe, catch_unwind}; +use state_chain::{StateChainCircuitInput, StateChainCircuitOutput, verify_sequencer_commit}; use zkm_primitives::io::ZKMPublicValues; -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; -use bitcoin::{ScriptBuf, TxOut, Txid, secp256k1::PublicKey}; +use bitcoin::{Transaction, secp256k1::XOnlyPublicKey}; pub use guest_executor::io::EthClientExecutorInput; use serde::{Deserialize, Serialize}; +use verifier::verify_groth16_proof; pub const GRAPH_ID_SIZE: usize = 16; pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; -pub const WATCHTOWER_COMMITMENT_PUBLIC_INPUTS_LEN_SIZE: usize = 4; -pub const WATCHTOWER_COMMITMENT_PROOF_PART_STARK_VK_LEN_SIZE: usize = 4; +pub const ZKM_VERSION_LEN_SIZE: usize = 4; pub const VK_HASH_SIZE: usize = 66; pub const TOTAL_WORK_SIZE: usize = 32; @@ -51,86 +46,156 @@ pub struct OperatorPublicOutputs { pub included_watchtowers: [u8; 32], } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct IndexedWatchtowerChallenge { + pub node_index: u16, + pub spv: SPV, +} + +/// Verifies that a proof identity matches both its output and Publisher authorization. +fn check_program_id( + actual_program_id: verifier::ProgramId, + output_program_id: verifier::ProgramId, + authorized_program_id: verifier::ProgramId, +) { + assert_eq!(actual_program_id, authorized_program_id, "unauthorized proof program id"); + assert_eq!(output_program_id, actual_program_id, "proof output program id mismatch"); +} + +fn checked_history_root( + program_type: verifier::ProgramType, + actual_program_id: verifier::ProgramId, + output_program_id: verifier::ProgramId, + authorized_program_id: verifier::ProgramId, + history: [u8; 32], +) -> [u8; 32] { + check_program_id(actual_program_id, output_program_id, authorized_program_id); + verifier::finalize_history(program_type, history, actual_program_id) +} + +/// Verifies that the latest Bitcoin commitment authorizes every supplied circuit proof. +fn verify_commitment_authorization( + commit_chain_output: &CommitChainCircuitOutput, + header_chain_output: &BlockHeaderCircuitOutput, + state_chain_output: &StateChainCircuitOutput, + header_program_id: verifier::ProgramId, + state_program_id: verifier::ProgramId, + commit_program_id: verifier::ProgramId, + sequencer_set_hash: [u8; 32], +) -> AuthorizedProgramIds { + let authorized = commit_chain_output.chain_state.authorized_program_ids; + authorized.validate().expect("invalid authorized ProgramIds"); + check_program_id(commit_program_id, commit_chain_output.self_program_id, authorized.commit); + let program_history_root = verifier::program_history_root( + checked_history_root( + verifier::ProgramType::Header, + header_program_id, + header_chain_output.self_program_id, + authorized.header, + header_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::State, + state_program_id, + state_chain_output.self_program_id, + authorized.state, + state_chain_output.program_history_hash, + ), + ); + let checkpoint_root = verifier::proof_checkpoint_root( + header_chain_output.upgrade_checkpoint_hash, + state_chain_output.upgrade_checkpoint_hash, + ); + assert_eq!( + checkpoint_root, commit_chain_output.chain_state.proof_checkpoint_root, + "proof checkpoint root mismatch" + ); + let commitment = + extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) + .expect("invalid commit-chain commitment output"); + assert_eq!( + commitment, + commit_chain_commitment_digest( + sequencer_set_hash, + state_chain_output.chain_state.genesis_evm_block_hash, + program_history_root, + checkpoint_root, + authorized, + ) + ); + authorized +} + +pub fn decode_operator_public_outputs( + public_values: &[u8], +) -> Result { + if public_values.len() != 96 { + return Err(format!( + "operator public values must be 96 bytes, got {}", + public_values.len() + )); + } + bincode::deserialize(public_values) + .map_err(|err| format!("failed to decode operator public values: {err}")) +} + pub fn watch_longest_chain( genesis_sequencer_commit_txid: [u8; 32], - latest_sequencer_commit_txid: [u8; 32], header_chain: HeaderChainCircuitInput, commit_chain: CommitChainCircuitInput, state_chain: StateChainCircuitInput, - attestation: WatchtowerAttestationInputs, spv: SPV, -) -> WatchtowerPublicOutputs { +) -> ([u8; 32], u32) { println!("commit header, size: {}", commit_chain.commits.len()); - let commit_chain_output = - verify_commit_chain_output(&commit_chain).expect("Failed to verify commit chain proof"); - let (publisher_public_keys, threshold) = - commit_chain_attestation_authority(&commit_chain_output); - verify_unique_part_stark_vk_witnesses( - &attestation.unique_witnesses, - publisher_public_keys, - threshold, - PART_STARK_VK_TREE_HEIGHT, + // verify latest_sequencer_commit is valid: + // * Check both latest_sequencer_commit_txid and genesis_sequencer_commit_txid are in all_sequencer_commit_txids (which is a private input) + // * Check latest_sequencer_commit_txid is derived from genesis_sequencer_commit_txid + // verify the commit chain proof + let commit_program_id = verify_groth16_proof( + &commit_chain.zkm_proof, + &commit_chain.zkm_public_values, + &commit_chain.zkm_vk_hash, + &commit_chain.zkm_version, ) - .expect("Failed to verify unique part_stark_vk attestations"); + .expect("Failed to verify commit chain proof"); + let commit_chain_output = decode_commit_chain_circuit_output(&commit_chain.zkm_public_values); assert_eq!( commit_chain_output.chain_state.commit_txn.compute_txid(), - Txid::from_byte_array(latest_sequencer_commit_txid) + spv.transaction.0.compute_txid() ); assert_eq!(genesis_sequencer_commit_txid, commit_chain_output.chain_state.genesis_txid); println!("header chain: applying: {}", header_chain.block_headers.len()); // verify header_chain is valid - let header_part_stark_vk = attested_part_stark_vk_for_zkm_version( - &attestation.unique_witnesses, - &header_chain.zkm_version, - ) - .expect("Failed to resolve attested header-chain part_stark_vk"); - verify_proof_with_part_stark_vk( + let header_program_id = verify_groth16_proof( &header_chain.zkm_proof, &header_chain.zkm_public_values, &header_chain.zkm_vk_hash, - &header_part_stark_vk, + &header_chain.zkm_version, ) .expect("Failed to verify header chain proof"); - let prev_output = ZKMPublicValues::from(&header_chain.zkm_public_values).read(); - let prev_proof = HeaderChainPrevProofType::PrevProof(prev_output); - let HeaderChainPrevProofType::PrevProof(btc_header_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in watch_longest_chain"); - }; - // verify that the latest_sequecner_commit_tx is in the header chain - println!("SPV"); - assert!(spv.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); + let btc_header_chain_output: BlockHeaderCircuitOutput = + ZKMPublicValues::from(&header_chain.zkm_public_values).read(); + assert_eq!( + btc_header_chain_output.chain_state.block_hashes_mmr.size, + btc_header_chain_output.chain_state.block_height + 1, + "header MMR size mismatch" + ); + let commitment_block_height = spv + .verify(&btc_header_chain_output.chain_state.block_hashes_mmr) + .expect("sequencer commitment SPV verification failed"); - let state_part_stark_vk = attested_part_stark_vk_for_zkm_version( - &attestation.unique_witnesses, - &state_chain.zkm_version, - ) - .expect("Failed to resolve attested state-chain part_stark_vk"); - verify_proof_with_part_stark_vk( + let state_program_id = verify_groth16_proof( &state_chain.zkm_proof, &state_chain.zkm_public_values, &state_chain.zkm_vk_hash, - &state_part_stark_vk, + &state_chain.zkm_version, ) .expect("Failed to verify state chain proof"); - let prev_output = ZKMPublicValues::from(&state_chain.zkm_public_values).read(); - let prev_proof = StateChainPrevProofType::PrevProof(prev_output); - let StateChainPrevProofType::PrevProof(state_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in watch_longest_chain"); - }; - assert_part_stark_vk_in_verified_witnesses( - &attestation.unique_witnesses, - &btc_header_chain_output.part_stark_vk, - ) - .expect("Failed to match header-chain part_stark_vk in verified witnesses"); - assert_part_stark_vk_in_verified_witnesses( - &attestation.unique_witnesses, - &state_chain_output.part_stark_vk, - ) - .expect("Failed to match state-chain part_stark_vk in verified witnesses"); - + let state_chain_output: StateChainCircuitOutput = + ZKMPublicValues::from(&state_chain.zkm_public_values).read(); // check the signature. let cosmos_block_bytes = &state_chain_output.chain_state.latest_cosmos_block; let cosmos_block: LightBlock = @@ -142,22 +207,23 @@ pub fn watch_longest_chain( let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); - // check commit chain's genesis block - let commitment = - commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); - if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment[0..32], x); + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + verify_commitment_authorization( + &commit_chain_output, + &btc_header_chain_output, + &state_chain_output, + header_program_id, + state_program_id, + commit_program_id, + sequencer_set_hash, + ); } else { panic!("Invalid commitment: inconsistent sequencer set hash"); }; - assert_eq!(commitment[32..64], state_chain_output.chain_state.genesis_evm_block_hash[..]); println!("commit public inputs"); // commit public inputs - WatchtowerPublicOutputs { - total_work: btc_header_chain_output.chain_state.total_work, - consensus_block_height: commit_chain_output.chain_state.block_height.to_le_bytes(), - } + (btc_header_chain_output.chain_state.total_work, commitment_block_height) } pub fn u256_to_le_bits(u: U256) -> [bool; 256] { @@ -178,6 +244,55 @@ pub fn le_bits_to_u256(bits: &[bool]) -> U256 { u } +/// Validates challenge indices against the graph-sized public inclusion bitmap. +pub fn validate_watchtower_challenge_indices( + included_watchtowers: &[bool; 256], + graph_watchtower_count: usize, + challenge_indices: &[u16], +) -> Result<(), String> { + if graph_watchtower_count == 0 || graph_watchtower_count > 256 { + return Err(format!("invalid graph watchtower count {graph_watchtower_count}")); + } + + let mut seen = [false; 256]; + for index in challenge_indices { + let index = *index as usize; + if index >= graph_watchtower_count { + return Err(format!("watchtower challenge index {index} out of bounds")); + } + if seen[index] { + return Err(format!("duplicate watchtower challenge index {index}")); + } + seen[index] = true; + } + if included_watchtowers != &seen { + return Err("watchtower challenge indices do not match included bitmap".to_string()); + } + Ok(()) +} + +/// Checks an optional challenge-init transaction against its graph txid. +/// A transaction is required when `challenges_present` is true. +fn checked_challenge_init_transaction( + expected_txid: [u8; 32], + transaction: Option<&Transaction>, + challenges_present: bool, +) -> Option<&Transaction> { + if let Some(transaction) = transaction { + assert_eq!( + transaction.compute_txid().to_byte_array(), + expected_txid, + "watchtower challenge init transaction txid mismatch" + ); + } + assert!( + !challenges_present || transaction.is_some(), + "watchtower challenge init transaction is required when challenges exist" + ); + + transaction +} + // calculate operator public input: https://github.com/ProjectZKM/Ziren/blob/main/crates/sdk/src/utils.rs#L42 #[allow(clippy::too_many_arguments)] pub fn propose_longest_chain( @@ -185,32 +300,27 @@ pub fn propose_longest_chain( graph_id: [u8; GRAPH_ID_SIZE], // pis operator_genesis_sequencer_commit_txid: [u8; 32], // pis - watchtower_challenge_txns: Vec, - watchtower_challenge_txn_pubkey: Vec, - watchtower_challenge_txn_scripts: Vec, - watchtower_challenge_txn_prev_outs: Vec, + watchtower_challenge_init_txid: [u8; 32], + watchtower_challenge_init_txn: Option, + watchtower_challenges: Vec, + graph_watchtower_xonly_public_keys: &[[u8; 32]], operator_header_chain: HeaderChainCircuitInput, commit_chain: CommitChainCircuitInput, state_chain: StateChainCircuitInput, - attestation: OperatorAttestationInputs, spv_ss_commit: SPV, operator_committed_blockhash: [u8; 32], -) -> OperatorPublicOutputs { +) -> ([u8; 32], [u8; 32], [u8; 32]) { // verify operator_latest_sequencer_commit_txid is valid, and on operator head chain // * Check operator_latest_sequencer_commit_txid is derived from genesis_sequencer_commit_txid - let commit_chain_output = - verify_commit_chain_output(&commit_chain).expect("Failed to verify commit chain proof"); - let (publisher_public_keys, threshold) = - commit_chain_attestation_authority(&commit_chain_output); - verify_unique_part_stark_vk_witnesses( - &attestation.unique_witnesses, - publisher_public_keys, - threshold, - PART_STARK_VK_TREE_HEIGHT, + let commit_program_id = verify_groth16_proof( + &commit_chain.zkm_proof, + &commit_chain.zkm_public_values, + &commit_chain.zkm_vk_hash, + &commit_chain.zkm_version, ) - .expect("Failed to verify unique part_stark_vk attestations"); - + .expect("Failed to verify commit chain proof"); + let commit_chain_output = decode_commit_chain_circuit_output(&commit_chain.zkm_public_values); assert_eq!( commit_chain_output.chain_state.commit_txn.compute_txid(), spv_ss_commit.transaction.0.compute_txid() @@ -222,100 +332,37 @@ pub fn propose_longest_chain( // https://github.com/KSlashh/BitVM/blob/v2/goat/src/transactions/watchtower_challenge.rs#L128 // verify operator_header_chain is valid - let header_part_stark_vk = attested_part_stark_vk_for_zkm_version( - &attestation.unique_witnesses, - &operator_header_chain.zkm_version, - ) - .expect("Failed to resolve attested header-chain part_stark_vk"); - verify_proof_with_part_stark_vk( + let header_program_id = verify_groth16_proof( &operator_header_chain.zkm_proof, &operator_header_chain.zkm_public_values, &operator_header_chain.zkm_vk_hash, - &header_part_stark_vk, + &operator_header_chain.zkm_version, ) .expect("Failed to verify header chain proof"); - let prev_output = ZKMPublicValues::from(&operator_header_chain.zkm_public_values).read(); - let prev_proof = HeaderChainPrevProofType::PrevProof(prev_output); - let HeaderChainPrevProofType::PrevProof(btc_header_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in propose_longest_chain"); - }; - assert_part_stark_vk_in_verified_witnesses( - &attestation.unique_witnesses, - &btc_header_chain_output.part_stark_vk, - ) - .expect("Failed to match header-chain part_stark_vk in verified witnesses"); + let btc_header_chain_output: BlockHeaderCircuitOutput = + ZKMPublicValues::from(&operator_header_chain.zkm_public_values).read(); let operator_total_work = btc_header_chain_output.chain_state.total_work; - let operator_consensus_block_height = U32::from(commit_chain_output.chain_state.block_height); - // commit header chain best block hash as pis let btc_best_block_hash = btc_header_chain_output.chain_state.best_block_hash; - - // verify that the latest_sequecner_commit_tx is in the header chain - assert!(spv_ss_commit.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); - - // parse included_watchtowers into bits array - let included_watchertowers_bits = u256_to_le_bits(included_watchtowers); - println!("included watchtowers:{included_watchertowers_bits:?}"); - // For each watchtowers, if the included_watchtowers[i] is true, - // verify the watchtower_challenge_txns[i] is valid - // verify watchtower_challenge_txns[i].total_work <= operator_header_chain.total_work - // verify watchtower_challenge_txns[i].epoch <= operator_latest_sequencer_commit_tx.epoch - for i in 0..watchtower_challenge_txns.len() { - if included_watchertowers_bits[i] { - let tx = &watchtower_challenge_txns[i]; - let prev_out = &watchtower_challenge_txn_prev_outs[i]; - let pubkey = &watchtower_challenge_txn_pubkey[i]; - let watchtower_outputs = verify_included_watchtower_challenge( - i, - &graph_id, - tx, - prev_out, - &watchtower_challenge_txn_scripts[i], - pubkey, - &attestation.unique_witnesses, - operator_total_work, - operator_consensus_block_height, - ) - .unwrap_or_else(|err| panic!("Watchtower[{i}] invalid included challenge: {err}")); - - println!( - "watchtower total work: {:?}", - U256::from_be_bytes(watchtower_outputs.total_work) - ); - println!("operator total work: {operator_total_work:?}"); - println!( - "watchtower_consensus_block_height : {:?}", - U32::from_le_bytes(watchtower_outputs.consensus_block_height) - ); - println!("operator_consensus_block_height : {operator_consensus_block_height:?}"); - } - } + assert_eq!( + btc_header_chain_output.chain_state.block_hashes_mmr.size, + btc_header_chain_output.chain_state.block_height + 1, + "header MMR size mismatch" + ); + let operator_consensus_block_height = spv_ss_commit + .verify(&btc_header_chain_output.chain_state.block_hashes_mmr) + .expect("sequencer commitment SPV verification failed"); println!("verify el block"); - - let state_part_stark_vk = attested_part_stark_vk_for_zkm_version( - &attestation.unique_witnesses, - &state_chain.zkm_version, - ) - .expect("Failed to resolve attested state-chain part_stark_vk"); - verify_proof_with_part_stark_vk( + let state_program_id = verify_groth16_proof( &state_chain.zkm_proof, &state_chain.zkm_public_values, &state_chain.zkm_vk_hash, - &state_part_stark_vk, + &state_chain.zkm_version, ) .expect("Failed to verify state chain proof"); - let prev_output = ZKMPublicValues::from(&state_chain.zkm_public_values).read(); - let prev_proof = StateChainPrevProofType::PrevProof(prev_output); - let StateChainPrevProofType::PrevProof(state_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in propose_longest_chain"); - }; - - assert_part_stark_vk_in_verified_witnesses( - &attestation.unique_witnesses, - &state_chain_output.part_stark_vk, - ) - .expect("Failed to match state-chain part_stark_vk in verified witnesses"); + let state_chain_output: StateChainCircuitOutput = + ZKMPublicValues::from(&state_chain.zkm_public_values).read(); // check the signature. let cosmos_block_bytes = &state_chain_output.chain_state.latest_cosmos_block; @@ -326,18 +373,100 @@ pub fn propose_longest_chain( let commit_sequencer_set_hash = sequencer_hash(&commit_chain_output.chain_state.sequencers); let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; - // check commit chain's genesis block - let commitment = - commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); - if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment[0..32], x); - } else { - panic!("Invalid commitment: inconsistent sequencer set hash"); - }; - assert_eq!(commitment[32..64], state_chain_output.chain_state.genesis_evm_block_hash[..]); - + let authorized = + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + verify_commitment_authorization( + &commit_chain_output, + &btc_header_chain_output, + &state_chain_output, + header_program_id, + state_program_id, + commit_program_id, + sequencer_set_hash, + ) + } else { + panic!("Invalid commitment: inconsistent sequencer set hash"); + }; assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); + let included_watchtowers_bits = u256_to_le_bits(included_watchtowers); + let challenge_indices = + watchtower_challenges.iter().map(|challenge| challenge.node_index).collect::>(); + validate_watchtower_challenge_indices( + &included_watchtowers_bits, + graph_watchtower_xonly_public_keys.len(), + &challenge_indices, + ) + .expect("invalid indexed watchtower challenges"); + + let watchtower_challenge_init_txn = checked_challenge_init_transaction( + watchtower_challenge_init_txid, + watchtower_challenge_init_txn.as_ref(), + !watchtower_challenges.is_empty(), + ); + for challenge in &watchtower_challenges { + let i = challenge.node_index as usize; + let challenge_height = challenge + .spv + .verify(&btc_header_chain_output.chain_state.block_hashes_mmr) + .expect("watchtower challenge SPV verification failed"); + assert!( + challenge_height <= btc_header_chain_output.chain_state.block_height, + "challenge height exceeds authenticated header chain" + ); + + let tx = &challenge.spv.transaction.0; + let input = tx.input.first().expect("watchtower challenge must have input 0"); + let expected_vout = u32::from(challenge.node_index) * 2; + assert_eq!(input.previous_output.txid.to_byte_array(), watchtower_challenge_init_txid); + assert_eq!(input.previous_output.vout, expected_vout); + + let prev_out = watchtower_challenge_init_txn + .expect("watchtower challenge init transaction is required") + .output + .get(expected_vout as usize) + .expect("watchtower challenge prevout is missing"); + let xonly = XOnlyPublicKey::from_slice(&graph_watchtower_xonly_public_keys[i]) + .expect("invalid graph watchtower x-only key"); + let script = bitcoin::blockdata::script::Builder::new() + .push_x_only_key(&xonly) + .push_opcode(bitcoin::opcodes::all::OP_CHECKSIG) + .into_script(); + verify_taproot_leaf_schnorr_signature(&script, tx, 0, prev_out, &xonly) + .expect("watchtower challenge signature verification failed"); + + let Ok(commitment) = extract_data_from_commitment_outputs(&tx.output) else { + continue; + }; + let Ok(( + parsed_graph_id, + proof, + public_values, + vk, + watchtower_total_work, + watchtower_consensus_block_height, + zkm_version, + )) = parse_watchtower_commitment(&commitment) + else { + continue; + }; + let Ok(program_id) = verify_groth16_proof(&proof, &public_values, &vk, &zkm_version) else { + continue; + }; + if program_id != authorized.watchtower || parsed_graph_id != graph_id { + continue; + } + assert!( + U256::from_be_bytes(watchtower_total_work) <= U256::from_be_bytes(operator_total_work), + "valid watchtower challenge has more work than operator" + ); + assert!( + u32::from_le_bytes(watchtower_consensus_block_height) + <= operator_consensus_block_height, + "valid watchtower challenge has a later commitment than operator" + ); + } + let mut is_found = false; for withdrawal in &state_chain_output.chain_state.withdrawals { if withdrawal.2.contains(&graph_id) { @@ -354,7 +483,12 @@ pub fn propose_longest_chain( "operator_genesis_sequencer_commit_txid hex: {:?}", hex::encode(operator_genesis_sequencer_commit_txid) ); - let constant = hash_operator_constant(graph_id, operator_genesis_sequencer_commit_txid); + let constant = hash_operator_constant( + graph_id, + operator_genesis_sequencer_commit_txid, + watchtower_challenge_init_txid, + graph_watchtower_xonly_public_keys, + ); println!("constant hex: {:?}", hex::encode(constant)); println!("btc_best_block_hash hex: {:?}", hex::encode(btc_best_block_hash)); @@ -370,20 +504,37 @@ pub fn propose_longest_chain( ); //operator_public_input - OperatorPublicOutputs { - btc_best_block_hash: operator_committed_blockhash, - constant, - included_watchtowers: included_watchtowers.to_le_bytes::<32>(), - } + (operator_committed_blockhash, constant, included_watchtowers.to_le_bytes::<32>()) } pub fn hash_operator_constant( graph_id: [u8; GRAPH_ID_SIZE], operator_genesis_sequencer_commit_txid: [u8; 32], + watchtower_challenge_init_txid: [u8; 32], + watchtower_xonly_public_keys: &[[u8; 32]], ) -> [u8; 32] { let mut engine = sha256::HashEngine::default(); + engine.input(b"bitvm/operator-constant/v3"); engine.input(&graph_id); engine.input(&operator_genesis_sequencer_commit_txid); + engine.input(&watchtower_challenge_init_txid); + engine.input(&(watchtower_xonly_public_keys.len() as u16).to_be_bytes()); + for key in watchtower_xonly_public_keys { + engine.input(key); + } + let hash = sha256::Hash::from_engine(engine); + *hash.as_byte_array() +} + +pub fn hash_partial_binding_witness( + constant: [u8; 32], + btc_best_block_hash: [u8; 32], + included_watchtowers: [u8; 32], +) -> [u8; 32] { + let mut engine = sha256::HashEngine::default(); + engine.input(&constant); + engine.input(&btc_best_block_hash); + engine.input(&included_watchtowers); let hash = sha256::Hash::from_engine(engine); *hash.as_byte_array() } @@ -452,54 +603,37 @@ pub fn build_spv( pub fn build_watchtower_commitment( graph_id: &[u8; GRAPH_ID_SIZE], - proof: &[u8], - public_inputs: &[u8], + proof: &[u8; PROOF_SIZE], + public_inputs: &[u8; PUBLIC_INPUTS_SIZE], vk_hash: &str, - proof_part_stark_vk: &[u8], -) -> Result, String> { - if proof.len() != PROOF_SIZE { - return Err(format!("invalid proof length: {}, expected {}", proof.len(), PROOF_SIZE)); - } - if proof_part_stark_vk.is_empty() { - return Err("proof_part_stark_vk must not be empty".to_string()); - } - let mut comm = Vec::with_capacity( - GRAPH_ID_SIZE - + PROOF_SIZE - + WATCHTOWER_COMMITMENT_PUBLIC_INPUTS_LEN_SIZE - + public_inputs.len() - + VK_HASH_SIZE - + WATCHTOWER_COMMITMENT_PROOF_PART_STARK_VK_LEN_SIZE - + proof_part_stark_vk.len(), - ); - comm.extend_from_slice(graph_id); + zkm_version: &str, +) -> Vec { + let mut comm = graph_id.to_vec(); comm.extend_from_slice(proof); - comm.extend_from_slice(&(public_inputs.len() as u32).to_le_bytes()); comm.extend_from_slice(public_inputs); - if vk_hash.len() != VK_HASH_SIZE { - return Err(format!( - "invalid vk_hash length: {}, expected {}", - vk_hash.len(), - VK_HASH_SIZE - )); - } + assert_eq!(vk_hash.len(), VK_HASH_SIZE); comm.extend_from_slice(vk_hash.as_bytes()); - comm.extend_from_slice(&(proof_part_stark_vk.len() as u32).to_le_bytes()); - comm.extend_from_slice(proof_part_stark_vk); - Ok(comm) + comm.extend_from_slice(&(zkm_version.len() as u32).to_le_bytes()); + comm.extend_from_slice(zkm_version.as_bytes()); + + comm } -pub type WatchtowerCommitmentResult = - ([u8; GRAPH_ID_SIZE], Vec, Vec, [u8; VK_HASH_SIZE], Vec); +pub type WatchtowerCommitmentResult = ( + [u8; GRAPH_ID_SIZE], + [u8; PROOF_SIZE], + [u8; PUBLIC_INPUTS_SIZE], + [u8; VK_HASH_SIZE], + [u8; TOTAL_WORK_SIZE], + [u8; CONSENSUS_BLOCK_HEIGHT_SIZE], + String, +); pub fn parse_watchtower_commitment( commitment: &[u8], ) -> Result { - let min_commitment_size = GRAPH_ID_SIZE - + PROOF_SIZE - + WATCHTOWER_COMMITMENT_PUBLIC_INPUTS_LEN_SIZE - + VK_HASH_SIZE - + WATCHTOWER_COMMITMENT_PROOF_PART_STARK_VK_LEN_SIZE; + let min_commitment_size = + GRAPH_ID_SIZE + PROOF_SIZE + PUBLIC_INPUTS_SIZE + VK_HASH_SIZE + ZKM_VERSION_LEN_SIZE; if commitment.len() < min_commitment_size { return Err(format!( "invalid commitment size: {}, expected at least {}", @@ -511,208 +645,68 @@ pub fn parse_watchtower_commitment( let mut graph_id = [0u8; GRAPH_ID_SIZE]; graph_id.copy_from_slice(&commitment[..end]); - let proof = commitment[end..end + PROOF_SIZE].to_vec(); + let mut proof = [0u8; PROOF_SIZE]; + proof.copy_from_slice(&commitment[end..end + PROOF_SIZE]); end += PROOF_SIZE; - let public_inputs_len = u32::from_le_bytes( - commitment[end..end + WATCHTOWER_COMMITMENT_PUBLIC_INPUTS_LEN_SIZE].try_into().unwrap(), - ) as usize; - end += WATCHTOWER_COMMITMENT_PUBLIC_INPUTS_LEN_SIZE; - - let proof_part_stark_vk_len_offset = end + public_inputs_len + VK_HASH_SIZE; - if commitment.len() - < proof_part_stark_vk_len_offset + WATCHTOWER_COMMITMENT_PROOF_PART_STARK_VK_LEN_SIZE - { - return Err(format!( - "invalid commitment size: {}, missing proof_part_stark_vk length field", - commitment.len() - )); - } - let proof_part_stark_vk_len = u32::from_le_bytes( - commitment[proof_part_stark_vk_len_offset - ..proof_part_stark_vk_len_offset + WATCHTOWER_COMMITMENT_PROOF_PART_STARK_VK_LEN_SIZE] - .try_into() - .unwrap(), - ) as usize; - let expected_size = min_commitment_size + public_inputs_len + proof_part_stark_vk_len; - if commitment.len() != expected_size { - return Err(format!( - "invalid commitment size: {}, expected {}", - commitment.len(), - expected_size - )); - } - - let zkm_public_values = commitment[end..end + public_inputs_len].to_vec(); - end += public_inputs_len; + let mut zkm_public_values = [0u8; PUBLIC_INPUTS_SIZE]; + zkm_public_values.copy_from_slice(&commitment[end..end + PUBLIC_INPUTS_SIZE]); + end += PUBLIC_INPUTS_SIZE; let mut zkm_vk_hash_bytes = [0u8; VK_HASH_SIZE]; zkm_vk_hash_bytes.copy_from_slice(&commitment[end..end + VK_HASH_SIZE]); end += VK_HASH_SIZE; - let proof_part_stark_vk_len = u32::from_le_bytes( - commitment[end..end + WATCHTOWER_COMMITMENT_PROOF_PART_STARK_VK_LEN_SIZE] - .try_into() - .unwrap(), - ) as usize; - if proof_part_stark_vk_len == 0 { - return Err("proof_part_stark_vk must not be empty".to_string()); + let zkm_version_len = + u32::from_le_bytes(commitment[end..end + ZKM_VERSION_LEN_SIZE].try_into().unwrap()) + as usize; + if zkm_version_len == 0 { + return Err("zkm_version must not be empty".to_string()); } - end += WATCHTOWER_COMMITMENT_PROOF_PART_STARK_VK_LEN_SIZE; - let proof_part_stark_vk = commitment[end..end + proof_part_stark_vk_len].to_vec(); - Ok((graph_id, proof, zkm_public_values, zkm_vk_hash_bytes, proof_part_stark_vk)) -} + end += ZKM_VERSION_LEN_SIZE; -pub fn parse_watchtower_public_outputs( - zkm_public_values: &[u8], -) -> Result { - let mut public_values = ZKMPublicValues::from(zkm_public_values); - catch_unwind(AssertUnwindSafe(|| public_values.read::())) - .map_err(|_| "failed to deserialize watchtower public outputs".to_string()) -} - -// Check the public values are consistent with the total work and block hash -fn groth16_verifier_keys(zkm_version: &str) -> Result<(&'static [u8], &'static [u8]), String> { - let imm_groth16_vk = *IMM_GROTH16_VK_BYTES; - let part_stark_vk = - catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version))) - .map_err(|_| format!("failed to load part_stark_vk for zkm_version '{zkm_version}'"))?; - Ok((imm_groth16_vk, part_stark_vk)) -} - -/// Resolve the version-derived `part_stark_vk` and require it to be attested before use. -fn attested_part_stark_vk_for_zkm_version( - unique_witnesses: &[UniquePartStarkVkWitness], - zkm_version: &str, -) -> Result, String> { - let (_, part_stark_vk) = groth16_verifier_keys(zkm_version)?; - assert_part_stark_vk_in_verified_witnesses(unique_witnesses, part_stark_vk)?; - Ok(part_stark_vk.to_vec()) -} - -/// Verify one included watchtower challenge end-to-end and return its parsed public outputs. -#[allow(clippy::too_many_arguments)] -fn verify_included_watchtower_challenge( - index: usize, - graph_id: &[u8; GRAPH_ID_SIZE], - tx: &Transaction, - prev_out: &TxOut, - script: &ScriptBuf, - pubkey: &PublicKey, - unique_witnesses: &[UniquePartStarkVkWitness], - operator_total_work: [u8; TOTAL_WORK_SIZE], - operator_consensus_block_height: U32, -) -> Result { - println!("Verify watchtower[{index}] tx: {}, {:?}", tx.compute_txid(), tx); - let input = tx - .input - .first() - .ok_or_else(|| "watchtower tx must contain at least one input".to_string())?; - let witness = input - .witness - .iter() - .next() - .ok_or_else(|| "watchtower tx witness must contain a taproot signature".to_string())?; - let sig = bitcoin::taproot::Signature::from_slice(witness) - .map_err(|err| format!("invalid taproot signature: {err}"))?; - let prev_index = input.previous_output.vout as usize; - verify_taproot_leaf_schnorr_signature(script, tx, prev_index, prev_out, pubkey, &sig) - .map_err(|err| format!("signature verification failed: {err}"))?; - - let commitment = extract_data_from_commitment_outputs(&tx.output); - println!("commitment: {commitment:?}"); - println!("commitment hex: {}", hex::encode(&commitment)); - - let (parsed_graph_id, proof, public_values, vk, proof_part_stark_vk) = - parse_watchtower_commitment(&commitment)?; - if parsed_graph_id != *graph_id { + let expected_size = min_commitment_size + zkm_version_len; + if commitment.len() != expected_size { return Err(format!( - "graph id mismatch: parsed={}, expected={}", - hex::encode(parsed_graph_id), - hex::encode(graph_id) + "invalid commitment size: {}, expected {}", + commitment.len(), + expected_size )); } - assert_part_stark_vk_in_verified_witnesses(unique_witnesses, &proof_part_stark_vk)?; - verify_proof_with_part_stark_vk(&proof, &public_values, &vk, &proof_part_stark_vk)?; - - println!("check total work with watchtower {index}"); - let watchtower_outputs = parse_watchtower_public_outputs(&public_values)?; - if U256::from_be_bytes(watchtower_outputs.total_work) > U256::from_be_bytes(operator_total_work) - { - return Err("watchtower total work exceeds operator total work".to_string()); - } - if U32::from_le_bytes(watchtower_outputs.consensus_block_height) - > operator_consensus_block_height - { - return Err( - "watchtower consensus block height exceeds operator consensus block height".to_string() - ); - } - Ok(watchtower_outputs) -} - -/// Verify commit-chain with the trusted base-layer verifier and return its output. -fn verify_commit_chain_output( - commit_chain: &CommitChainCircuitInput, -) -> Result { - let trusted_part_stark_vk = commit_chain::trusted_commit_chain_part_stark_vk(); - verify_proof_with_part_stark_vk( - &commit_chain.zkm_proof, - &commit_chain.zkm_public_values, - &commit_chain.zkm_vk_hash, - &trusted_part_stark_vk, - )?; - let output: CommitChainCircuitOutput = - ZKMPublicValues::from(&commit_chain.zkm_public_values).read(); - - Ok(output) -} + let zkm_version = String::from_utf8(commitment[end..end + zkm_version_len].to_vec()) + .map_err(|err| format!("invalid zkm_version UTF-8: {err}"))?; -/// Return the publisher set that authorizes part_stark_vk attestations for this commit-chain output. -fn commit_chain_attestation_authority( - commit_chain_output: &CommitChainCircuitOutput, -) -> (&[PublicKey], u16) { - ( - &commit_chain_output.chain_state.publisher_public_keys, - commit_chain_output.chain_state.threshold, - ) -} + // extract ChainState + let mut watchtower_total_work = [0u8; TOTAL_WORK_SIZE]; + watchtower_total_work.copy_from_slice(&zkm_public_values[0..TOTAL_WORK_SIZE]); + let mut watchtower_consensus_block_height = [0u8; CONSENSUS_BLOCK_HEIGHT_SIZE]; + watchtower_consensus_block_height.copy_from_slice( + &zkm_public_values[TOTAL_WORK_SIZE..TOTAL_WORK_SIZE + CONSENSUS_BLOCK_HEIGHT_SIZE], + ); -pub fn verify_proof( - proof: &[u8], - zkm_public_values: &[u8], - zkm_vk_hash: &[u8], - zkm_version: &str, -) -> Result<(), String> { - let (_, part_stark_vk) = groth16_verifier_keys(zkm_version)?; - verify_proof_with_part_stark_vk(proof, zkm_public_values, zkm_vk_hash, part_stark_vk) -} + println!("watchtower total work: {watchtower_total_work:?}"); + println!("watchtower total work: {:?}", U256::from_be_bytes(watchtower_total_work)); + println!("watchtower consensus block height: {watchtower_consensus_block_height:?}"); + println!( + "watchtower consensus block height: {:?}", + U32::from_le_bytes(watchtower_consensus_block_height) + ); -/// Verify a Groth16 proof against an explicit part_stark_vk instead of a version lookup. -pub fn verify_proof_with_part_stark_vk( - proof: &[u8], - zkm_public_values: &[u8], - zkm_vk_hash: &[u8], - part_stark_vk: &[u8], -) -> Result<(), String> { - let groth16_vk = *IMM_GROTH16_VK_BYTES; - let zkm_vk_hash = String::from_utf8(zkm_vk_hash.to_vec()).map_err(|e| e.to_string())?; - match Groth16Verifier::verify_by_imm_groth16_vk( + Ok(( + graph_id, proof, zkm_public_values, - &zkm_vk_hash, - groth16_vk, - part_stark_vk, - ) { - Ok(_) => Ok(()), - Err(err) => Err(format!("Verify Groth16 proof, err: {err:?}")), - } + zkm_vk_hash_bytes, + watchtower_total_work, + watchtower_consensus_block_height, + zkm_version, + )) } #[cfg(test)] mod tests { use super::*; - use crate::PartStarkVkAttestationBundle; + use bitcoin::Transaction; const PROOF: &[u8] = include_bytes!("../../../circuits/data/watchtower/output3.bin.proof.bin"); const PUBLIC_INPUTS: &[u8] = @@ -721,42 +715,73 @@ mod tests { const ZKM_VERSION: &str = "v1.2.4"; #[test] - fn test_groth16_verifier_keys_keep_common_vk_available() { - assert!(!IMM_GROTH16_VK_BYTES.is_empty()); - - match groth16_verifier_keys(ZKM_VERSION) { - Ok((imm_groth16_vk, part_stark_vk)) => { - assert_eq!(imm_groth16_vk, *IMM_GROTH16_VK_BYTES); - assert!(!part_stark_vk.is_empty()); - } - Err(err) => { - assert!(err.contains("failed to load part_stark_vk")); - } - } + fn checked_history_root_binds_actual_output_and_expected_program_ids() { + let program_id = [1u8; 32]; + let history = [2u8; 32]; + assert_eq!( + checked_history_root( + verifier::ProgramType::Header, + program_id, + program_id, + program_id, + history, + ), + verifier::finalize_history(verifier::ProgramType::Header, history, program_id) + ); + + let wrong_expected = std::panic::catch_unwind(|| { + checked_history_root( + verifier::ProgramType::Header, + program_id, + program_id, + [3u8; 32], + history, + ) + }); + assert!(wrong_expected.is_err()); + + let wrong_output = std::panic::catch_unwind(|| { + checked_history_root( + verifier::ProgramType::Header, + program_id, + [3u8; 32], + program_id, + history, + ) + }); + assert!(wrong_output.is_err()); + } + + #[test] + fn check_program_id_rejects_unauthorized_or_mismatched_outputs() { + let program_id = [1u8; 32]; + check_program_id(program_id, program_id, program_id); + + assert!( + std::panic::catch_unwind(|| check_program_id(program_id, program_id, [2u8; 32])) + .is_err() + ); + assert!( + std::panic::catch_unwind(|| check_program_id(program_id, [2u8; 32], program_id)) + .is_err() + ); } #[test] fn test_build_watchtower_commitment() { let graph_id = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); - let total_work = 1006120u64; - let block_height = 503043u32; - let proof_part_stark_vk = vec![8u8; 52]; - let expected_outputs = WatchtowerPublicOutputs { - total_work: U256::from(total_work).to_be_bytes(), - consensus_block_height: U32::from(block_height).to_le_bytes(), - }; - let public_inputs = bincode::serialize(&expected_outputs).unwrap(); + let total_work = 1006120; + let block_height = 503043; println!("public inputs: {:?}", PUBLIC_INPUTS.len()); println!("vk hash: {:?}", VK_HASH.len()); let comm = build_watchtower_commitment( &graph_id, - PROOF, - &public_inputs, + &PROOF.try_into().unwrap(), + &PUBLIC_INPUTS.try_into().unwrap(), VK_HASH, - &proof_part_stark_vk, - ) - .unwrap(); + ZKM_VERSION, + ); println!("comm: {:?}", comm.len()); println!("comm hex: {:?}", hex::encode(&comm)); @@ -765,11 +790,11 @@ mod tests { assert_eq!(expected.0, graph_id); assert_eq!(expected.1, PROOF); - assert_eq!(expected.2, public_inputs); + assert_eq!(expected.2, PUBLIC_INPUTS); assert_eq!(expected.3, VK_HASH.as_bytes()); - assert_eq!(expected.4, proof_part_stark_vk); - let parsed_outputs = parse_watchtower_public_outputs(&expected.2).unwrap(); - assert_eq!(parsed_outputs, expected_outputs); + assert_eq!(expected.4, U256::from(total_work).to_be_bytes()); + assert_eq!(expected.5, U32::from(block_height).to_le_bytes()); + assert_eq!(expected.6, ZKM_VERSION.to_string()); } #[test] @@ -783,6 +808,93 @@ mod tests { assert_eq!(words, recovered); } + #[test] + fn test_hash_partial_binding_witness() { + use bitcoin::hashes::Hash as _; + + let constant = [1u8; 32]; + let btc_best_block_hash = [2u8; 32]; + let included_watchtowers = [3u8; 32]; + let mut input = Vec::new(); + input.extend_from_slice(&constant); + input.extend_from_slice(&btc_best_block_hash); + input.extend_from_slice(&included_watchtowers); + let expected = bitcoin::hashes::sha256::Hash::hash(&input); + + assert_eq!( + hash_partial_binding_witness(constant, btc_best_block_hash, included_watchtowers), + *expected.as_byte_array() + ); + } + + #[test] + fn test_hash_operator_constant_binds_ordered_watchtower_keys() { + use bitcoin::hashes::Hash as _; + + let graph_id = [1u8; GRAPH_ID_SIZE]; + let genesis_txid = [2u8; 32]; + let watchtower_keys = [[3u8; 32], [4u8; 32]]; + let challenge_init_txid = [5u8; 32]; + let mut input = b"bitvm/operator-constant/v3".to_vec(); + input.extend_from_slice(&graph_id); + input.extend_from_slice(&genesis_txid); + input.extend_from_slice(&challenge_init_txid); + input.extend_from_slice(&(watchtower_keys.len() as u16).to_be_bytes()); + for key in &watchtower_keys { + input.extend_from_slice(key); + } + let expected = bitcoin::hashes::sha256::Hash::hash(&input); + + assert_eq!( + hash_operator_constant(graph_id, genesis_txid, challenge_init_txid, &watchtower_keys), + *expected.as_byte_array() + ); + assert_ne!( + hash_operator_constant( + graph_id, + genesis_txid, + challenge_init_txid, + &[watchtower_keys[1], watchtower_keys[0]], + ), + *expected.as_byte_array() + ); + } + + #[test] + fn optional_challenge_init_transaction_is_fail_closed() { + let transaction = Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + let txid = transaction.compute_txid().to_byte_array(); + + assert!(checked_challenge_init_transaction(txid, None, false).is_none()); + assert!(checked_challenge_init_transaction(txid, Some(&transaction), true).is_some()); + assert!( + std::panic::catch_unwind(|| { + checked_challenge_init_transaction([1u8; 32], Some(&transaction), false) + }) + .is_err() + ); + assert!( + std::panic::catch_unwind(|| { checked_challenge_init_transaction(txid, None, true) }) + .is_err() + ); + } + + #[test] + fn watchtower_challenge_indices_preserve_sparse_bitmap_positions() { + let mut included = [false; 256]; + included[1] = true; + included[4] = true; + + validate_watchtower_challenge_indices(&included, 5, &[1, 4]).unwrap(); + assert!(validate_watchtower_challenge_indices(&included, 5, &[0, 1]).is_err()); + assert!(validate_watchtower_challenge_indices(&included, 5, &[1, 1]).is_err()); + } + #[test] fn test_u256_to_le_bits() { use std::str::FromStr; @@ -811,222 +923,46 @@ mod tests { ).unwrap(); let tx: Transaction = deserialize(&bytes).unwrap(); - let commitment = extract_data_from_commitment_outputs(&tx.output); + let commitment = extract_data_from_commitment_outputs(&tx.output).unwrap(); let parse_result = parse_watchtower_commitment(&commitment); assert!(parse_result.is_err(), "legacy commitment with trailing zkm_version should fail"); } #[test] - fn test_parse_watchtower_commitment_rejects_legacy_v1_payload() { - let graph_id: [u8; GRAPH_ID_SIZE] = - hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); - let mut legacy = graph_id.to_vec(); - legacy.extend_from_slice(PROOF); - legacy.extend_from_slice(&(PUBLIC_INPUTS.len() as u32).to_le_bytes()); - legacy.extend_from_slice(PUBLIC_INPUTS); - legacy.extend_from_slice(VK_HASH.as_bytes()); - let mut legacy_zkm_version = [0u8; 16]; - legacy_zkm_version[..ZKM_VERSION.len()].copy_from_slice(ZKM_VERSION.as_bytes()); - legacy.extend_from_slice(&legacy_zkm_version); - assert!(parse_watchtower_commitment(&legacy).is_err()); - } - - #[test] - fn test_parse_watchtower_commitment_rejects_missing_proof_part_stark_vk() { + fn test_parse_watchtower_commitment_rejects_missing_zkm_version_len() { let graph_id: [u8; GRAPH_ID_SIZE] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); let mut commitment = graph_id.to_vec(); commitment.extend_from_slice(PROOF); - commitment.extend_from_slice(&(PUBLIC_INPUTS.len() as u32).to_le_bytes()); commitment.extend_from_slice(PUBLIC_INPUTS); commitment.extend_from_slice(VK_HASH.as_bytes()); assert!(parse_watchtower_commitment(&commitment).is_err()); } #[test] - fn test_parse_watchtower_commitment_accepts_versionless_dual_key_payload() { + fn test_parse_watchtower_commitment_rejects_empty_zkm_version() { let graph_id: [u8; GRAPH_ID_SIZE] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); - let proof_part_stark_vk = vec![9u8; 48]; let mut commitment = graph_id.to_vec(); commitment.extend_from_slice(PROOF); - commitment.extend_from_slice(&(PUBLIC_INPUTS.len() as u32).to_le_bytes()); commitment.extend_from_slice(PUBLIC_INPUTS); commitment.extend_from_slice(VK_HASH.as_bytes()); - commitment.extend_from_slice(&(proof_part_stark_vk.len() as u32).to_le_bytes()); - commitment.extend_from_slice(&proof_part_stark_vk); + commitment.extend_from_slice(&0u32.to_le_bytes()); - assert!( - parse_watchtower_commitment(&commitment).is_ok(), - "versionless dual-key commitment should parse" - ); + assert!(parse_watchtower_commitment(&commitment).is_err()); } #[test] - fn test_parse_watchtower_commitment_rejects_empty_proof_part_stark_vk() { + fn test_parse_watchtower_commitment_rejects_invalid_zkm_version_utf8() { let graph_id: [u8; GRAPH_ID_SIZE] = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); let mut commitment = graph_id.to_vec(); commitment.extend_from_slice(PROOF); - commitment.extend_from_slice(&(PUBLIC_INPUTS.len() as u32).to_le_bytes()); commitment.extend_from_slice(PUBLIC_INPUTS); commitment.extend_from_slice(VK_HASH.as_bytes()); - commitment.extend_from_slice(&0u32.to_le_bytes()); + commitment.extend_from_slice(&2u32.to_le_bytes()); + commitment.extend_from_slice(&[0xff, 0xff]); assert!(parse_watchtower_commitment(&commitment).is_err()); } - - #[test] - fn test_parse_watchtower_commitment_rejects_versioned_payload() { - let graph_id: [u8; GRAPH_ID_SIZE] = - hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); - let proof_part_stark_vk = vec![3u8; 12]; - let mut versioned = vec![2u8]; - versioned.extend_from_slice(&graph_id); - versioned.extend_from_slice(PROOF); - versioned.extend_from_slice(&(PUBLIC_INPUTS.len() as u32).to_le_bytes()); - versioned.extend_from_slice(PUBLIC_INPUTS); - versioned.extend_from_slice(VK_HASH.as_bytes()); - versioned.extend_from_slice(&(proof_part_stark_vk.len() as u32).to_le_bytes()); - versioned.extend_from_slice(&proof_part_stark_vk); - - assert!( - parse_watchtower_commitment(&versioned).is_err(), - "versioned watchtower commitment should be rejected" - ); - } - - #[test] - fn test_parse_watchtower_public_outputs_rejects_short_public_inputs() { - let result = parse_watchtower_public_outputs(&[0u8; TOTAL_WORK_SIZE + 1]); - assert!(result.is_err()); - } - - #[test] - fn test_parse_watchtower_public_outputs_reads_bincode_serialized_struct() { - let expected = WatchtowerPublicOutputs { - total_work: [9u8; TOTAL_WORK_SIZE], - consensus_block_height: 123u32.to_le_bytes(), - }; - - let public_inputs = bincode::serialize(&expected).unwrap(); - let parsed = parse_watchtower_public_outputs(&public_inputs).unwrap(); - - assert_eq!(parsed, expected); - } - - #[test] - fn test_verify_proof_accepts_non_fixed_length_version() { - let long_version = "v1.12.15-rc1+build.20260319"; - let result = verify_proof(&[], &[], &[], long_version); - assert!(result.is_err()); - assert!(!result.unwrap_err().contains("too long")); - } - - #[test] - fn test_verify_proof_with_part_stark_vk_uses_explicit_vk_bytes() { - let part_stark_vk = groth16_verifier_keys(ZKM_VERSION).unwrap().1.to_vec(); - let result = verify_proof_with_part_stark_vk(&[], &[], &[], &part_stark_vk); - assert!(result.is_err()); - } - - #[test] - fn test_groth16_verifier_keys_reject_unknown_version_without_panic() { - let result = groth16_verifier_keys("v0.0.0-test"); - assert!(result.is_err()); - } - - fn sample_unique_witness(part_stark_vk: Vec) -> PartStarkVkAttestationBundle { - PartStarkVkAttestationBundle { - part_stark_vk, - leaf_index: 0, - merkle_path: vec![], - root: [0u8; 32], - threshold: 1, - publisher_set_id: [0u8; 32], - signatures: vec![], - } - } - - #[test] - fn test_attested_part_stark_vk_for_zkm_version_accepts_verified_witness_payload() { - let part_stark_vk = groth16_verifier_keys(ZKM_VERSION).unwrap().1.to_vec(); - let unique_witnesses = vec![sample_unique_witness(part_stark_vk.clone())]; - - assert_eq!( - attested_part_stark_vk_for_zkm_version(&unique_witnesses, ZKM_VERSION).unwrap(), - part_stark_vk - ); - } - - #[test] - fn test_attested_part_stark_vk_for_zkm_version_rejects_missing_witness_payload() { - let unique_witnesses = vec![sample_unique_witness(vec![7u8; 32])]; - - assert!(attested_part_stark_vk_for_zkm_version(&unique_witnesses, ZKM_VERSION).is_err()); - } - - fn sample_commit_tx() -> Transaction { - use bitcoin::{absolute::LockTime, transaction::Version}; - - Transaction { - version: Version::TWO, - lock_time: LockTime::ZERO, - input: vec![], - output: vec![], - } - } - - fn sample_commit_chain_output( - genesis_txid: [u8; 32], - commit_txn: Transaction, - publisher_public_keys: Vec, - threshold: u16, - ) -> CommitChainCircuitOutput { - CommitChainCircuitOutput { - chain_state: commit_chain::CommitChainState { - block_height: 7, - commit_txn, - genesis_txid, - sequencers: vec![], - publisher_public_keys, - threshold, - }, - } - } - - #[test] - fn test_commit_chain_attestation_authority_uses_chain_state() { - let publisher_public_keys = - commit_chain::create_dummy_publisher_keys(3, bitcoin::Network::Regtest) - .into_iter() - .map(|(_, pk)| pk) - .collect::>(); - let threshold = 2u16; - let commit_chain_output = sample_commit_chain_output( - [3u8; 32], - sample_commit_tx(), - publisher_public_keys.clone(), - threshold, - ); - - let (actual_keys, actual_threshold) = - commit_chain_attestation_authority(&commit_chain_output); - assert_eq!(actual_keys, publisher_public_keys.as_slice()); - assert_eq!(actual_threshold, threshold); - } - - #[test] - fn test_operator_public_outputs_bincode_shape_excludes_part_stark_vk() { - let expected = OperatorPublicOutputs { - btc_best_block_hash: [1u8; 32], - constant: [2u8; 32], - included_watchtowers: [3u8; 32], - }; - - let public_inputs = bincode::serialize(&expected).unwrap(); - let parsed: OperatorPublicOutputs = bincode::deserialize(&public_inputs).unwrap(); - - assert_eq!(parsed, expected); - } } diff --git a/crates/bitcoin-light-client-circuit/src/signature.rs b/crates/bitcoin-light-client-circuit/src/signature.rs index 43f145885..c789f696f 100644 --- a/crates/bitcoin-light-client-circuit/src/signature.rs +++ b/crates/bitcoin-light-client-circuit/src/signature.rs @@ -7,9 +7,9 @@ pub use tendermint_light_client_verifier::{ use bitcoin::{ Script, ScriptBuf, Transaction, TxOut, key::Keypair, - secp256k1::{Message as EcdsaMessage, PublicKey, Secp256k1, XOnlyPublicKey}, + secp256k1::{Message as EcdsaMessage, Secp256k1, XOnlyPublicKey}, sighash::{Prevouts, SighashCache, TapSighashType}, - taproot::{LeafVersion, Signature as TaprootSignature, TapLeafHash}, + taproot::{ControlBlock, LeafVersion, Signature as TaprootSignature, TapLeafHash}, }; /// Generate Taproot script-path's Schnorr signature @@ -40,26 +40,47 @@ fn generate_taproot_leaf_schnorr_signature( TaprootSignature { signature: sig, sighash_type } } -/// Verify Schnorr signature -/// +/// Verifies the Taproot script-path witness and Schnorr signature for one transaction input. pub fn verify_taproot_leaf_schnorr_signature( script: &ScriptBuf, spending_tx: &Transaction, - prev_index: usize, + input_index: usize, prev_out: &TxOut, - pubkey: &PublicKey, - sig: &TaprootSignature, + pubkey: &XOnlyPublicKey, ) -> Result<(), Box> { + let input = spending_tx.input.get(input_index).ok_or("Invalid input index")?; + let sig = input.witness.iter().next().ok_or("Missing Taproot signature").and_then(|bytes| { + TaprootSignature::from_slice(bytes).map_err(|_| "Invalid Taproot signature") + })?; if sig.sighash_type != TapSighashType::AllPlusAnyoneCanPay { return Err("Invalid sig type".into()); } let secp = Secp256k1::verification_only(); + let witness_len = input.witness.len(); + if witness_len < 3 { + return Err("Invalid Taproot script-path witness".into()); + } + let witness_script = + input.witness.iter().nth(witness_len - 2).ok_or("Missing Taproot witness script")?; + if witness_script != script.as_bytes() { + return Err("Taproot witness script mismatch".into()); + } + let control_block = ControlBlock::decode( + input.witness.iter().nth(witness_len - 1).ok_or("Missing Taproot control block")?, + )?; + let script_pubkey = prev_out.script_pubkey.as_bytes(); + if script_pubkey.len() != 34 || script_pubkey[0] != 0x51 || script_pubkey[1] != 0x20 { + return Err("Prevout is not P2TR".into()); + } + let output_key = XOnlyPublicKey::from_slice(&script_pubkey[2..])?; + if !control_block.verify_taproot_commitment(&secp, output_key, script) { + return Err("Taproot control block does not commit to the witness script".into()); + } + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); - let internal_xonly: XOnlyPublicKey = (*pubkey).into(); let sighash = match SighashCache::new(spending_tx).taproot_script_spend_signature_hash( - 0, - //&Prevouts::All(&[prev_out.clone()]), - &Prevouts::One(prev_index, prev_out.clone()), + input_index, + &Prevouts::One(input_index, prev_out.clone()), leaf_hash, TapSighashType::AllPlusAnyoneCanPay, ) { @@ -68,7 +89,7 @@ pub fn verify_taproot_leaf_schnorr_signature( }; let msg = EcdsaMessage::from(sighash); - Ok(secp.verify_schnorr(&sig.signature, &msg, &internal_xonly)?) + Ok(secp.verify_schnorr(&sig.signature, &msg, pubkey)?) } #[cfg(test)] @@ -127,39 +148,58 @@ mod tests { }], output: vec![TxOut { value: Amount::from_sat(49_000), - script_pubkey: ScriptBuf::new_op_return(&[0x6a]), + script_pubkey: ScriptBuf::new_op_return([0x6a]), }], }; // 6. Generate Schnorr signature let sig = generate_taproot_leaf_schnorr_signature( &mut spending_tx, - &[prev_out.clone()], + std::slice::from_ref(&prev_out), 0, TapSighashType::AllPlusAnyoneCanPay, &script, &keypair, ); - // 7. Verify the signature - verify_taproot_leaf_schnorr_signature( - &script, - &spending_tx, - 0, - &prev_out, - &keypair.public_key(), - &sig, - ) - .unwrap(); - println!("Schnorr signature verified successfully!"); - - // 8. Construct control block + witness + // 7. Construct control block + witness let control_block = taproot_info .control_block(&(script.clone(), LeafVersion::TapScript)) .expect("control block"); - spending_tx.input[0].witness = - Witness::from(vec![sig.to_vec(), script.into_bytes(), control_block.serialize()]); + spending_tx.input[0].witness = Witness::from(vec![ + sig.to_vec(), + script.clone().into_bytes(), + control_block.serialize(), + ]); + + // 8. Verify the signature and Taproot script commitment. + verify_taproot_leaf_schnorr_signature(&script, &spending_tx, 0, &prev_out, &internal_xonly) + .unwrap(); + let mut wrong_prevout = prev_out.clone(); + wrong_prevout.script_pubkey = ScriptBuf::new(); + assert!( + verify_taproot_leaf_schnorr_signature( + &script, + &spending_tx, + 0, + &wrong_prevout, + &internal_xonly, + ) + .is_err() + ); + let mut missing_signature = spending_tx.clone(); + missing_signature.input[0].witness = Witness::new(); + assert!( + verify_taproot_leaf_schnorr_signature( + &script, + &missing_signature, + 0, + &prev_out, + &internal_xonly, + ) + .is_err() + ); println!("Final spending tx hex = {}", hex::encode(serialize(&spending_tx))); } diff --git a/crates/bitcoin-light-client-circuit/src/utils.rs b/crates/bitcoin-light-client-circuit/src/utils.rs index 02ee439cb..01f45231f 100644 --- a/crates/bitcoin-light-client-circuit/src/utils.rs +++ b/crates/bitcoin-light-client-circuit/src/utils.rs @@ -1,6 +1,7 @@ use bitcoin::absolute::LockTime; use bitcoin::blockdata::opcodes::all::*; use bitcoin::blockdata::script::Builder; +use bitcoin::script::PushBytesBuf; use bitcoin::transaction::Version; use bitcoin::{Address, Amount, OutPoint, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Witness}; @@ -81,7 +82,7 @@ pub fn create_fee_tx( } pub fn create_sequencer_update_partial_tx( - commitment: [u8; 64], + commitment: [u8; 32], update_connector: &Option, replenish_fee_connector: &Option, next_update_connector: Address, @@ -96,6 +97,8 @@ pub fn create_sequencer_update_partial_tx( println!("commitment: {commitment:?}"); + let commitment = + PushBytesBuf::try_from(commitment.to_vec()).expect("commitment payload is pushable"); let script = Builder::new().push_opcode(OP_RETURN).push_slice(commitment).into_script(); // make TxOut with 0 satoshis diff --git a/crates/bitvm2-ga/Cargo.toml b/crates/bitvm-gc/Cargo.toml similarity index 61% rename from crates/bitvm2-ga/Cargo.toml rename to crates/bitvm-gc/Cargo.toml index 4210348bb..50ae4e378 100644 --- a/crates/bitvm2-ga/Cargo.toml +++ b/crates/bitvm-gc/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "bitvm2-lib" +name = "bitvm-gc" edition = { workspace = true } version = { workspace = true } @@ -7,9 +7,6 @@ version = { workspace = true } bitvm = { workspace = true } goat = { workspace = true } serde = { workspace = true } -ark-groth16 = { workspace = true } -ark-bn254 = { workspace = true } -ark-serialize = { workspace = true } sha2 = { workspace = true } hkdf = { workspace = true } chacha20poly1305 = { workspace = true } @@ -20,9 +17,17 @@ secp256k1 = { workspace = true } anyhow = { workspace = true } hex = { workspace = true } bitcoin-script = { workspace = true } -bitcoin-light-client-circuit = { workspace = true } tracing = { workspace = true } +ark-bn254 = { workspace = true } +ark-groth16 = { workspace = true } +ark-serialize = { workspace = true } +ark-ff = { workspace = true } +verifiable-circuit-babe = { workspace = true } +garbled-snark-verifier = { workspace = true } +soldering-host = { workspace = true } +rayon = { workspace = true } +serde-big-array = { workspace = true } serde_json = { workspace = true } bincode.workspace = true uuid = { workspace = true } @@ -30,12 +35,14 @@ clap = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] } [dev-dependencies] -client = { workspace = true } +ark-crypto-primitives = { workspace = true } +ark-ec = { workspace = true } esplora-client = { workspace = true } -tokio = { workspace = true, features = ["full"] } -bitcoincore-rpc = "0.19" -bitcoin-old = { version = "0.31.2", package = "bitcoin" } +rand_chacha = { workspace = true } +reqwest = { workspace = true } +reqwest-0-11 = { package = "reqwest", version = "0.11.27", default-features = false, features = ["json", "rustls-tls"] } +tokio = { workspace = true, features = ["macros", "time"] } [features] default = [] -ci-tests = [] \ No newline at end of file +ci-tests = [] diff --git a/crates/bitvm2-ga/src/actors.rs b/crates/bitvm-gc/src/actors.rs similarity index 93% rename from crates/bitvm2-ga/src/actors.rs rename to crates/bitvm-gc/src/actors.rs index 7f664bb35..747e7d7b9 100644 --- a/crates/bitvm2-ga/src/actors.rs +++ b/crates/bitvm-gc/src/actors.rs @@ -5,7 +5,7 @@ use strum::{Display, EnumString}; pub enum Actor { Committee, Operator, - Challenger, + Verifier, Watchtower, Publisher, All, diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs new file mode 100644 index 000000000..ab9c6ce02 --- /dev/null +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -0,0 +1,649 @@ +use std::collections::HashSet; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::PathBuf; + +use crate::types::BitvmGcCircuitData; +use anyhow::{Result, bail}; +use ark_bn254::g1::G1Affine; +use ark_bn254::{Bn254, Fr}; +use ark_groth16::Proof as Groth16Proof; +use ark_groth16::VerifyingKey as Groth16VerifyingKey; +use ark_serialize::CanonicalSerialize; +use garbled_snark_verifier::bag::S; +use goat::assert_scripts::{INPUT_WIRE_NUM, OperatorAssertSecretKey, WireHash, label_hash}; +use goat::wots::{Wots, Wots96}; +use serde::{Deserialize, Serialize}; +use sha2::{Digest, Sha256}; +use soldering_host::BabeBundle; +pub use soldering_host::BabeBundleBuilder; +use verifiable_circuit_babe::babe::{ + GC_INPUT_WIRES, WitnessEncSetupCt, + build_challenge_assert_witness as babe_build_challenge_assert_witness, +}; +use verifiable_circuit_babe::cac::cac_finalize_indices; +pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; +use verifiable_circuit_babe::dre::N_PADDED; +use verifiable_circuit_babe::gc::{SGC_PART1_CONSTANT_SIZE, SparseAdaptorTable}; +pub use verifiable_circuit_babe::instance::commit::CACInstanceCommit; +use verifiable_circuit_babe::prover::BABEProver; +use verifiable_circuit_babe::soldering::{ + SolderedLabelsData, SolderingData as RealSolderingData, SolderingProof as RealSolderingProof, +}; +pub use verifiable_circuit_babe::transactions::ChallengeAssertWitnessRaw; +pub use verifiable_circuit_babe::transactions::TxAssertWitness; +use verifiable_circuit_babe::utils::pi1_xd_to_wots96_msg; +use verifiable_circuit_babe::verifier::BABEVerifier; + +/// Number of Wots96 digit signatures expected by the GOAT GC-V2 connector. +pub const WOTS_SIG_COUNT: usize = Wots96::TOTAL_DIGIT_LEN as usize; +pub const BABE_N_CC: usize = 181; +pub const BABE_M_CC: usize = 7; + +pub type OpenedInstanceSeeds = Vec<(usize, u64)>; +pub type FinalizedInstances = Vec; +pub type SetupAndSolderingData = (OpenedInstanceSeeds, FinalizedInstances, SolderingData); + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CompactSolderingData { + pub finalized_indices: Vec, + pub proof: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CompactSolderingProofPayload { + pub opened: OpenedInstanceSeeds, + pub finalized: Vec, + pub soldering: CompactSolderingData, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SolderingData { + pub finalized_indices: Vec, + pub soldered_output: SolderedLabelsData, + pub proof: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeVerifierPrivateState { + pub instance_seeds: Vec, + pub statement_digest: [u8; 32], +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeVerifierState { + pub package: CACSetupPackage, + pub finalized_indices: Vec, + pub verifier_pubkey: bitcoin::PublicKey, +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeProverState { + pub package: CACSetupPackage, + pub finalized: Vec, + pub soldering: SolderingData, + pub h_msgs: Vec<[u8; 20]>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeChallengeAssertWitness { + pub verifier_index: usize, + pub witness: ChallengeAssertWitnessRaw, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeWronglyChallengedWitness { + pub verifier_index: usize, + // just need to contain one valid msg + pub final_msg: Vec, +} + +/// Builds deterministic placeholder setup commitments for tests and wiring. +pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { + let epk = (0..GC_INPUT_WIRES) + .map(|wire| { + let w = (wire as u16).to_le_bytes(); + [hash20(&[seed, w[0], w[1], 0]), hash20(&[seed, w[0], w[1], 1])] + }) + .collect(); + + let constant_commit_sgc: Vec<_> = (0..SGC_PART1_CONSTANT_SIZE) + .map(|_| [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])]) + .collect(); + + CACInstanceCommit { + epk, + constant_commits_fgc: [ + [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])], + [hash32(&[seed, 0xf1, 0]), hash32(&[seed, 0xf1, 1])], + ], + constant_commits_sgc: constant_commit_sgc, + b_blind_commit: hash32(&[seed, 0xa0]), + h_msg: hash20(&[seed, 0xa1]), + h_ct_setup: hash32(&[seed, 0xa2]), + com_adaptor: [hash32(&[seed, 0xa3]), hash32(&[seed, 0xa4])], + com_gc: [hash32(&[seed, 0xa5]), hash32(&[seed, 0xa6]), hash32(&[seed, 0xa7])], + } +} + +pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { + let adaptor_tables = + [SparseAdaptorTable { entries: vec![] }, SparseAdaptorTable { entries: vec![] }]; + let ct_setup = WitnessEncSetupCt { ct2_r_delta_g2: vec![], ct3_masked_msg: vec![] }; + + FinalizedInstanceData { + index, + ciphertext_sets: [vec![], vec![], vec![]], + adaptor_tables, + ct_setup, + constant_labels_0: [S([0; 16]), S([0; 16])], + constant_labels_1: vec![], + b: G1Affine::identity(), + aes_salt: S([0; 16]), + } +} + +impl SolderingData { + /// Builds deterministic placeholder soldering data for the selected finalized indices. + pub fn sample(finalized_indices: Vec) -> Self { + Self { finalized_indices, soldered_output: SolderedLabelsData::default(), proof: vec![] } + } +} + +/// Builds a deterministic placeholder CAC setup package with `n_cc` instances. +pub fn build_setup_package(n_cc: usize) -> Result { + if n_cc == 0 { + bail!("n_cc must be greater than zero"); + } + Ok(CACSetupPackage { + commits: (0..n_cc).map(|index| sample_cac_instance_commit(index as u8)).collect(), + }) +} + +/// Builds a real random BABE/CAC setup package bound to the supplied Groth16 statement. +pub fn build_real_setup_package( + n_cc: usize, + vk: &Groth16VerifyingKey, + static_inputs: Fr, +) -> Result<(CACSetupPackage, BabeVerifierPrivateState)> { + if n_cc == 0 { + bail!("n_cc must be greater than zero"); + } + ensure_real_gc_assets_configured()?; + let verifier = catch_unwind(AssertUnwindSafe(|| BABEVerifier::new(n_cc, vk, static_inputs))) + .map_err(|_| anyhow::anyhow!("real BABE verifier setup panicked while loading GC assets"))? + .map_err(anyhow::Error::msg)?; + let package = verifier.commit(); + let private_state = BabeVerifierPrivateState { + instance_seeds: verifier.get_seeds(), + statement_digest: statement_digest(vk, static_inputs)?, + }; + + Ok((package, private_state)) +} + +/// Reconstructs the real Verifier instances and creates CAC opening/soldering output data. +pub fn open_real_setup_and_solder( + soldering_builder: &BabeBundleBuilder, + private_state: &BabeVerifierPrivateState, + package: &CACSetupPackage, + finalized_indices: &[usize], + vk: &Groth16VerifyingKey, + static_inputs: Fr, +) -> Result { + ensure_real_gc_assets_configured()?; + if private_state.statement_digest != statement_digest(vk, static_inputs)? { + bail!("BABE setup statement does not match persisted verifier state"); + } + validate_finalized_indices(package, finalized_indices)?; + let verifier = restore_real_verifier(private_state, package, vk, static_inputs)?; + if verifier.commit() != *package { + bail!("persisted BABE verifier state does not reproduce setup package"); + } + let bundle = soldering_builder + .babe_verifier_open_and_solder(&verifier, finalized_indices) + .map_err(anyhow::Error::msg)?; + Ok((bundle.opened, bundle.finalized, from_real_soldering(&bundle.soldering)?)) +} + +/// Verifies real CAC openings, commitments, and the native Ziren soldering proof. +#[allow(clippy::too_many_arguments)] +pub fn verify_real_setup( + soldering_builder: &BabeBundleBuilder, + package: &CACSetupPackage, + opened: &[(usize, u64)], + finalized: &[FinalizedInstanceData], + soldering: &SolderingData, + vk: &Groth16VerifyingKey, + claimed_finalized_indices: &[usize], + static_public_inputs: Fr, +) -> Result<()> { + let bundle = BabeBundle { + opened: opened.to_vec(), + finalized: finalized.to_vec(), + soldering: to_real_soldering(soldering)?, + }; + soldering_builder + .babe_prover_verify_setup( + package, + &bundle, + vk, + static_public_inputs, + claimed_finalized_indices, + ) + .map_err(anyhow::Error::msg) +} + +/// Removes setup-derived public fields from the Verifier-to-Operator soldering proof payload. +pub fn compact_soldering_proof_payload( + opened: &[(usize, u64)], + finalized: &[FinalizedInstanceData], + soldering: &SolderingData, +) -> Result { + Ok(CompactSolderingProofPayload { + opened: opened.to_vec(), + finalized: finalized.to_vec(), + soldering: CompactSolderingData { + finalized_indices: soldering.finalized_indices.clone(), + proof: soldering.proof.clone(), + }, + }) +} + +/// Reconstructs the full BABE setup data using the locally trusted setup package. +pub fn expand_compact_soldering_proof_payload( + payload: CompactSolderingProofPayload, +) -> Result { + let finalized = payload.finalized.clone(); + Ok((payload.opened, finalized, expand_compact_soldering_data(payload.soldering)?)) +} + +/// Derives finalized circuit indices using the real BABE/CAC Fiat-Shamir selection. +pub fn derive_finalized_indices(package: &CACSetupPackage, m_cc: usize) -> Result> { + let n_cc = package.commits.len(); + if m_cc == 0 || m_cc > n_cc { + bail!("invalid m_cc {m_cc} for n_cc {n_cc}"); + } + Ok(cac_finalize_indices(n_cc, m_cc)) +} + +/// Opens non-finalized placeholder instances and returns finalized data plus soldering data. +pub fn open_and_solder( + package: &CACSetupPackage, + finalized_indices: &[usize], +) -> Result { + let finalized_set = finalized_indices.iter().copied().collect::>(); + if finalized_set.len() != finalized_indices.len() { + bail!("duplicate finalized index"); + } + if finalized_indices.iter().any(|index| *index >= package.commits.len()) { + bail!("finalized index out of range"); + } + let opened = (0..package.commits.len()) + .filter(|index| !finalized_set.contains(index)) + .map(|index| (index, deterministic_seed(index))) + .collect::>(); + let finalized = + finalized_indices.iter().map(|index| sample_finalized_instance_data(*index)).collect(); + let soldering = SolderingData::sample(finalized_indices.to_vec()); + Ok((opened, finalized, soldering)) +} + +/// Validates placeholder opened, finalized, and soldering data consistency. +pub fn verify_setup( + package: &CACSetupPackage, + opened: &[(usize, u64)], + finalized: &[FinalizedInstanceData], + soldering: &SolderingData, +) -> Result<()> { + let n_cc = package.commits.len(); + let finalized_set = finalized.iter().map(|data| data.index).collect::>(); + if finalized_set.len() != finalized.len() { + bail!("duplicate finalized data"); + } + for (index, seed) in opened { + if *index >= n_cc { + bail!("opened index {index} out of range"); + } + if finalized_set.contains(index) { + bail!("index {index} cannot be both opened and finalized"); + } + if *seed != deterministic_seed(*index) { + bail!("opened seed mismatch for index {index}"); + } + } + if soldering.finalized_indices != finalized.iter().map(|data| data.index).collect::>() { + bail!("soldering finalized indices mismatch"); + } + for data in finalized { + if data.index >= n_cc { + bail!("finalized index {} out of range", data.index); + } + } + Ok(()) +} + +/// Extracts one graph slot owned by `verifier_pubkey` from finalized setup data. +/// +/// `epk` must have exactly `GC_INPUT_WIRES` (768) entries +pub fn extract_gc_circuit_data( + verifier_pubkey: bitcoin::PublicKey, + epk: &[[[u8; 20]; 2]], + h_msgs: &[[u8; 20]], +) -> Result { + if epk.len() != GC_INPUT_WIRES { + bail!("BABE epk has {} entries; expected {GC_INPUT_WIRES}", epk.len()); + } + let to_wire_hash = + |pair: &[[u8; 20]; 2]| WireHash { false_label_hash: pair[0], true_label_hash: pair[1] }; + + let wire_hashes: [WireHash; INPUT_WIRE_NUM] = epk + .iter() + .map(to_wire_hash) + .collect::>() + .try_into() + .map_err(|v: Vec| { + anyhow::anyhow!("wire hash count {} does not match expected {INPUT_WIRE_NUM}", v.len()) + })?; + Ok(BitvmGcCircuitData { verifier_pubkey, final_msg_hashlocks: h_msgs.to_vec(), wire_hashes }) +} + +/// Builds the native BABE assertion witness from the validated operator Groth16 proof. +pub fn build_assert_witness( + proof: &ark_groth16::Proof, + assert_secret_key: &OperatorAssertSecretKey, + dynamic_input: ark_bn254::Fr, +) -> Result { + if assert_secret_key.is_empty() { + bail!("operator WOTS secret key must not be empty"); + } + let msg = pi1_xd_to_wots96_msg(&proof.a, dynamic_input); + let wots_sig = Wots96::sign(assert_secret_key, &msg); + let mut pi2 = Vec::new(); + let mut pi3 = Vec::new(); + proof.b.serialize_compressed(&mut pi2)?; + proof.c.serialize_compressed(&mut pi3)?; + Ok(TxAssertWitness { wots_sig: wots_sig.to_vec(), pi2, pi3 }) +} + +pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> { + let arr_sig: [[u8; 21]; Wots96::TOTAL_DIGIT_LEN as usize] = + assert_witness.wots_sig.clone().try_into().map_err(|_| { + anyhow::anyhow!("WOTS signature has wrong length; expected {}", Wots96::TOTAL_DIGIT_LEN) + })?; + Ok(Wots96::signature_to_message(&arr_sig)) +} + +pub fn recover_operator_proof_from_assert_witness( + assert_witness: &TxAssertWitness, +) -> Result> { + let (pi1, _) = assert_witness + .try_recover_pi1_xd() + .ok_or_else(|| anyhow::anyhow!("Cannot recover pi1 and xd"))?; + let (pi2, pi3) = assert_witness + .recover_pi2_pi3() + .ok_or_else(|| anyhow::anyhow!("Cannot recover pi2 and pi3"))?; + Ok(Groth16Proof { a: pi1, b: pi2, c: pi3 }) +} + +/// Builds a placeholder verifier challenge witness from an assert witness. +pub fn build_challenge_assert_witness( + verifier_state: &BabeVerifierState, + assert_witness: &TxAssertWitness, + verifier_index: usize, +) -> Result { + if verifier_state.finalized_indices.len() != BABE_M_CC { + bail!("verifier state must contain exactly {BABE_M_CC} finalized BABE instances"); + } + let bytes = assert_wots_message(assert_witness)?; + Ok(BabeChallengeAssertWitness { + verifier_index, + witness: ChallengeAssertWitnessRaw { + input_labels: (0usize..INPUT_WIRE_NUM) + .map(|index| hash16_with_index(&bytes, index)) + .collect(), + wots_sig: assert_witness.wots_sig.clone(), + }, + }) +} + +/// Verifies a native operator assertion and reveals the real base-instance labels. +pub fn build_real_challenge_assert_witness( + private_state: &BabeVerifierPrivateState, + package: &CACSetupPackage, + finalized_indices: &[usize], + vk: &Groth16VerifyingKey, + static_inputs: Fr, + assert_witness: &TxAssertWitness, + verifier_index: usize, +) -> Result { + if finalized_indices.len() != BABE_M_CC { + bail!("verifier state must contain exactly {BABE_M_CC} finalized BABE instances"); + } + let verifier = restore_real_verifier(private_state, package, vk, static_inputs)?; + if verifier.commit() != *package { + bail!("persisted BABE verifier state does not reproduce setup package"); + } + + let base_idx = finalized_indices[0]; + let witness = babe_build_challenge_assert_witness(&verifier, assert_witness, base_idx); + + if witness.witness.input_labels.len() != GC_INPUT_WIRES { + bail!( + "real BABE challenge labels have {}; expected {GC_INPUT_WIRES}", + witness.witness.input_labels.len() + ); + } + Ok(BabeChallengeAssertWitness { verifier_index, witness: witness.witness }) +} + +/// Builds a wrongly-challenged witness from a valid recovered finalized-message preimage. +pub fn build_wrongly_challenged_witness( + prover_state: &BabeProverState, + challenge_witness: &BabeChallengeAssertWitness, + final_msg: Vec, +) -> Result { + build_wrongly_challenged_witness_from_preimages( + &prover_state.h_msgs, + challenge_witness, + final_msg, + ) +} + +/// Evaluates the native BABE garbled circuit and returns a finalized hashlock preimages. +pub fn recover_real_wrongly_challenged_witness( + prover_state: &BabeProverState, + challenge_witness: &BabeChallengeAssertWitness, + proof: &ark_groth16::Proof, + vk: Groth16VerifyingKey, + dyn_pubin: ark_bn254::Fr, +) -> Result { + let mut prover = BABEProver::new(vk, proof.clone(), dyn_pubin); + recover_a_valid_finalized_messages(prover_state, challenge_witness, &mut prover) +} + +/// Builds a wrongly-challenged witness after validating all finalized preimages. +pub fn build_wrongly_challenged_witness_from_preimages( + h_msgs: &[[u8; 20]], + challenge_witness: &BabeChallengeAssertWitness, + final_msg: Vec, +) -> Result { + if h_msgs.len() != BABE_M_CC { + bail!("wrongly challenged setup must contain exactly {BABE_M_CC} finalized hashlocks"); + } + + if !h_msgs.contains(&label_hash(&final_msg)) { + bail!("message is not a valid preimage"); + } + + Ok(BabeWronglyChallengedWitness { verifier_index: challenge_witness.verifier_index, final_msg }) +} + +fn ensure_real_gc_assets_configured() -> Result<()> { + for name in [ + "FGC_GATES_PATH", + "FGC_OUT_INDICES_PATH", + "SGC_GATES_PATH", + "SGC_OUT_INDICES_PATH", + "FGC_COMPACT_GATES_PATH", + "FGC_COMPACT_OUT_INDICES_PATH", + "SGC_COMPACT_GATES_PATH", + "SGC_COMPACT_OUT_INDICES_PATH", + ] { + let path = PathBuf::from( + std::env::var(name) + .map_err(|_| anyhow::anyhow!("{name} is required for real CAC setup"))?, + ); + if !path.is_file() { + bail!("{name} does not point to a readable file: {}", path.display()); + } + } + Ok(()) +} + +fn statement_digest(vk: &Groth16VerifyingKey, static_inputs: Fr) -> Result<[u8; 32]> { + let mut bytes = Vec::new(); + vk.serialize_compressed(&mut bytes)?; + static_inputs.serialize_compressed(&mut bytes)?; + Ok(hash32(&bytes)) +} + +fn restore_real_verifier( + state: &BabeVerifierPrivateState, + package: &CACSetupPackage, + vk: &Groth16VerifyingKey, + static_inputs: Fr, +) -> Result { + BABEVerifier::from_state(&state.instance_seeds, package, vk, static_inputs) + .ok_or_else(|| anyhow::anyhow!("Cannot restore real verifier")) +} + +fn validate_finalized_indices( + package: &CACSetupPackage, + finalized_indices: &[usize], +) -> Result<()> { + let finalized_set = finalized_indices.iter().copied().collect::>(); + if finalized_set.len() != finalized_indices.len() { + bail!("duplicate finalized index"); + } + if finalized_indices.is_empty() { + bail!("at least one finalized index is required"); + } + if finalized_indices.iter().any(|index| *index >= package.commits.len()) { + bail!("finalized index out of range"); + } + Ok(()) +} + +fn from_real_soldering(soldering: &RealSolderingData) -> Result { + let output = soldering.soldering_proof.output().map_err(anyhow::Error::msg)?; + Ok(SolderingData { + finalized_indices: soldering.finalized_indices.clone(), + soldered_output: SolderedLabelsData { + base_commitment: output.base_commitment.clone(), + deltas: output.deltas.clone(), + commitments: output.commitments.clone(), + }, + proof: bincode::serialize(&soldering.soldering_proof.proof)?, + }) +} + +fn expand_compact_soldering_data(soldering: CompactSolderingData) -> Result { + if soldering.proof.is_empty() { + bail!("soldering proof is empty"); + } + let proof = RealSolderingProof { proof: bincode::deserialize(&soldering.proof)? }; + let output = proof.output().map_err(anyhow::Error::msg)?; + Ok(SolderingData { + finalized_indices: soldering.finalized_indices, + soldered_output: SolderedLabelsData { + base_commitment: output.base_commitment.clone(), + deltas: output.deltas.clone(), + commitments: output.commitments.clone(), + }, + proof: soldering.proof, + }) +} + +fn to_real_soldering(soldering: &SolderingData) -> Result { + if soldering.proof.is_empty() { + bail!("soldering proof is empty"); + } + Ok(RealSolderingData { + finalized_indices: soldering.finalized_indices.clone(), + soldering_proof: RealSolderingProof { proof: bincode::deserialize(&soldering.proof)? }, + }) +} + +fn recover_a_valid_finalized_messages( + prover_state: &BabeProverState, + challenge_witness: &BabeChallengeAssertWitness, + prover: &mut BABEProver, +) -> Result { + if prover_state.finalized.len() != prover_state.h_msgs.len() { + bail!("BABE prover state finalized data and hash count differ"); + } + if prover_state.finalized.is_empty() { + bail!("BABE prover state has no finalized instances"); + } + + let base_input_labels: Vec = + challenge_witness.witness.input_labels.iter().map(|&b| S(b)).collect(); + let pi1_full_labels = base_input_labels[..2 * N_PADDED].to_vec(); + let x_d_full_labels = base_input_labels[2 * N_PADDED..].to_vec(); + let real_soldering = to_real_soldering(&prover_state.soldering)?; + + let soldered_output = &prover_state.soldering.soldered_output; + if soldered_output.base_commitment.len() != GC_INPUT_WIRES { + bail!( + "soldering base commitment count {} does not match expected GC input wire count {GC_INPUT_WIRES}", + soldered_output.base_commitment.len() + ); + } + + let found = prover.check_compute_msg( + &prover_state.finalized, + &pi1_full_labels, + &x_d_full_labels, + &real_soldering, + &prover_state.h_msgs, + ); + + if !found { + bail!("Cannot find any valid msg"); + } + + Ok(BabeWronglyChallengedWitness { + verifier_index: challenge_witness.verifier_index, + final_msg: prover + .valid_msg + .ok_or_else(|| { + anyhow::anyhow!("check_compute_msg returned true but valid_msg is not set") + })? + .to_vec(), + }) +} + +fn deterministic_seed(index: usize) -> u64 { + u64::from_le_bytes(hash32(&(index as u64).to_le_bytes())[0..8].try_into().expect("8 bytes")) +} + +fn hash20(data: &[u8]) -> [u8; 20] { + let hash = hash32(data); + hash[0..20].try_into().expect("20 bytes") +} + +fn hash16(data: &[u8]) -> [u8; 16] { + let hash = hash32(data); + hash[0..16].try_into().expect("16 bytes") +} + +fn hash16_with_index(data: &[u8], index: usize) -> [u8; 16] { + let mut bytes = Vec::with_capacity(data.len() + std::mem::size_of::()); + bytes.extend_from_slice(data); + bytes.extend_from_slice(&(index as u64).to_le_bytes()); + hash16(&bytes) +} + +fn hash32(data: &[u8]) -> [u8; 32] { + Sha256::digest(data).into() +} diff --git a/crates/bitvm-gc/src/committee/api.rs b/crates/bitvm-gc/src/committee/api.rs new file mode 100644 index 000000000..dfbcc8b29 --- /dev/null +++ b/crates/bitvm-gc/src/committee/api.rs @@ -0,0 +1,999 @@ +use anyhow::{Result, bail, ensure}; +use bitcoin::XOnlyPublicKey; +use bitcoin::{ + PublicKey, Script, TapLeafHash, TapSighash, TapSighashType, Transaction, TxOut, + key::Keypair, + sighash::{Prevouts, SighashCache}, + taproot::{ControlBlock, LeafVersion, Signature as TaprootSignature}, +}; +use goat::contexts::base::generate_n_of_n_public_key; +use goat::transactions::base::BaseTransaction; +use goat::transactions::pre_signed::PreSignedTransaction; +use goat::transactions::pre_signed_musig2::{get_nonce_message, verify_public_nonce}; +use goat::transactions::signing_musig2::generate_aggregated_nonce; +use musig2::secp::Point; +use musig2::{AggNonce, KeyAggContext, PartialSignature, PubNonce, SecNonce, verify_partial}; +use secp256k1::{Message, SECP256K1, schnorr::Signature as SchnorrSignature}; +use serde::{Deserialize, Serialize}; + +use crate::keys::hkdf_derive_bytes; +use crate::types::{BitvmGcGraph, BitvmGcInstanceParameters}; + +const COMMITTEE_NONCE_HKDF_SALT: &[u8] = b"bitvm-gc/committee-nonce/v1"; + +pub fn take1_pre_sign_num() -> usize { + 2 +} +pub fn take2_pre_sign_num() -> usize { + 1 +} +pub fn challenge_pre_sign_num() -> usize { + 1 +} +pub fn watchtower_challenge_timeout_pre_sign_num(watchtower_num: usize) -> usize { + watchtower_num +} +pub fn operator_challenge_nack_pre_sign_num(watchtower_num: usize) -> usize { + watchtower_num * 2 +} +pub fn operator_commit_timeout_pre_sign_num() -> usize { + 2 +} +pub fn disprove_pre_sign_num(verifier_num: usize) -> usize { + verifier_num * 2 +} + +pub fn sign_pegin_confirm( + graph: &BitvmGcGraph, + committee_member_keypair: Keypair, + committee_member_sec_nonce: SecNonce, + committee_agg_nonce: AggNonce, +) -> Result { + let mut pegin_confirm = graph.parameters.instance_parameters.build_pegin_tx()?.1; + let committee_context = + graph.parameters.instance_parameters.get_committee_context(committee_member_keypair)?; + pegin_confirm + .sign_input_0_musig2(&committee_context, &committee_member_sec_nonce, &committee_agg_nonce) + .map_err(|e| anyhow::anyhow!("fail to sign pegin confirm {}: {e}", pegin_confirm.name())) +} + +pub fn agg_and_push_pegin_confirm_sigs( + graph: &BitvmGcGraph, + partial_sigs: Vec, + agg_nonce: &AggNonce, +) -> Result { + let mut pegin_confirm = graph.parameters.instance_parameters.build_pegin_tx()?.1; + let context = graph.parameters.instance_parameters.get_base_context(); + let agg_sig = pegin_confirm + .aggregate_input_0_musig2_signatures(&context, partial_sigs, agg_nonce) + .map_err(|e| { + anyhow::anyhow!("fail to aggregate pegin confirm {}: {e}", pegin_confirm.name()) + })?; + let connector_z = graph.parameters.instance_parameters.connector_z(); + pegin_confirm.push_input_0_signature(&connector_z, agg_sig); + Ok(pegin_confirm.finalize()) +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CommitteeMusig2Data { + pub take1: Vec, + pub take2: Vec, + pub challenge: Vec, + pub watchtower_challenge_timeout: Vec, + pub operator_challenge_nack: Vec, + pub operator_commit_timeout: Vec, + pub disprove: Vec, +} + +pub type CommitteeSecNonces = CommitteeMusig2Data; +pub type CommitteePubNonces = CommitteeMusig2Data; +pub type CommitteeNonceSignatures = CommitteeMusig2Data; +pub type CommitteeAggNonces = CommitteeMusig2Data; +pub type CommitteePartialSignatures = CommitteeMusig2Data; +pub type CommitteeSignatures = CommitteeMusig2Data; + +impl CommitteeMusig2Data { + pub fn validate_length(&self, watchtower_num: usize, verifier_num: usize) -> Result<()> { + ensure!(self.take1.len() == take1_pre_sign_num(), "invalid number of take1"); + ensure!(self.take2.len() == take2_pre_sign_num(), "invalid number of take2"); + ensure!(self.challenge.len() == challenge_pre_sign_num(), "invalid number of challenge"); + ensure!( + self.watchtower_challenge_timeout.len() + == watchtower_challenge_timeout_pre_sign_num(watchtower_num), + "invalid number of watchtower challenge timeout" + ); + ensure!( + self.operator_challenge_nack.len() + == operator_challenge_nack_pre_sign_num(watchtower_num), + "invalid number of operator challenge nack" + ); + ensure!( + self.operator_commit_timeout.len() == operator_commit_timeout_pre_sign_num(), + "invalid number of operator commit timeout" + ); + ensure!( + self.disprove.len() == disprove_pre_sign_num(verifier_num), + "invalid number of disprove" + ); + Ok(()) + } + + pub fn new_empty() -> Self { + CommitteeMusig2Data { + take1: vec![], + take2: vec![], + challenge: vec![], + watchtower_challenge_timeout: vec![], + operator_challenge_nack: vec![], + operator_commit_timeout: vec![], + disprove: vec![], + } + } +} + +pub fn key_aggregation(pubkeys: &[PublicKey]) -> PublicKey { + generate_n_of_n_public_key(pubkeys).0 +} + +pub fn committee_pre_sign( + committee_member_keypair: Keypair, + committee_member_sec_nonce: CommitteeSecNonces, + committee_agg_nonce: CommitteeAggNonces, + graph: &mut BitvmGcGraph, +) -> Result { + let verifier_num = graph.verifier_asserts.len(); + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + committee_member_sec_nonce.validate_length(watchtower_num, verifier_num)?; + committee_agg_nonce.validate_length(watchtower_num, verifier_num)?; + + let committee_context = + graph.parameters.instance_parameters.get_committee_context(committee_member_keypair)?; + let mut res = CommitteePartialSignatures::new_empty(); + + { + // take-1 + let sec_nonces = committee_member_sec_nonce.take1.try_into().unwrap(); + let agg_nonces = committee_agg_nonce.take1.try_into().unwrap(); + match graph.take1.pre_sign(&committee_context, &sec_nonces, &agg_nonces) { + Ok(v) => res.take1 = v.to_vec(), + Err(e) => bail!("fail to pre-sign {}: {e}", graph.take1.name()), + }; + } + + { + // take-2 + let sec_nonces = committee_member_sec_nonce.take2.try_into().unwrap(); + let agg_nonces = committee_agg_nonce.take2.try_into().unwrap(); + match graph.take2.pre_sign(&committee_context, &sec_nonces, &agg_nonces) { + Ok(v) => res.take2 = v.to_vec(), + Err(e) => bail!("fail to pre-sign {}: {e}", graph.take2.name()), + }; + } + + { + // challenge + let sec_nonces = committee_member_sec_nonce.challenge.try_into().unwrap(); + let agg_nonces = committee_agg_nonce.challenge.try_into().unwrap(); + match graph.challenge.pre_sign(&committee_context, &sec_nonces, &agg_nonces) { + Ok(v) => res.challenge = v.to_vec(), + Err(e) => bail!("fail to pre-sign {}: {e}", graph.challenge.name()), + }; + } + + { + // watchtower challenge timeout + let mut timeout_sigs = vec![]; + for (i, tx) in graph.watchtower_challenge_timeouts.iter_mut().enumerate() { + let sec_nonces = [committee_member_sec_nonce.watchtower_challenge_timeout[i].clone()]; + let agg_nonces = [committee_agg_nonce.watchtower_challenge_timeout[i].clone()]; + let sigs = tx + .pre_sign(&committee_context, &sec_nonces, &agg_nonces) + .map_err(|e| anyhow::anyhow!("fail to pre-sign {}: {e}", tx.name()))?; + timeout_sigs.extend(sigs); + } + res.watchtower_challenge_timeout = timeout_sigs; + } + + { + // operator challenge nack + let mut nack_sigs = vec![]; + for (i, tx) in graph.operator_challenge_nacks.iter_mut().enumerate() { + let sec_nonces = [ + committee_member_sec_nonce.operator_challenge_nack[i * 2].clone(), + committee_member_sec_nonce.operator_challenge_nack[i * 2 + 1].clone(), + ]; + let agg_nonces = [ + committee_agg_nonce.operator_challenge_nack[i * 2].clone(), + committee_agg_nonce.operator_challenge_nack[i * 2 + 1].clone(), + ]; + let sigs = tx + .pre_sign(&committee_context, &sec_nonces, &agg_nonces) + .map_err(|e| anyhow::anyhow!("fail to pre-sign {}: {e}", tx.name()))?; + nack_sigs.extend(sigs); + } + res.operator_challenge_nack = nack_sigs; + } + + { + // operator commit timeout + let sec_nonces = committee_member_sec_nonce.operator_commit_timeout.try_into().unwrap(); + let agg_nonces = committee_agg_nonce.operator_commit_timeout.try_into().unwrap(); + match graph.operator_commit_timeout.pre_sign(&committee_context, &sec_nonces, &agg_nonces) { + Ok(v) => res.operator_commit_timeout = v.to_vec(), + Err(e) => bail!("fail to pre-sign {}: {e}", graph.operator_commit_timeout.name()), + }; + } + + { + // disprove + let mut disprove_sigs = vec![]; + for (i, disprove_tx) in graph.disproves.iter_mut().enumerate() { + let sec_nonces = [ + committee_member_sec_nonce.disprove[i * 2].clone(), + committee_member_sec_nonce.disprove[i * 2 + 1].clone(), + ]; + let agg_nonces = [ + committee_agg_nonce.disprove[i * 2].clone(), + committee_agg_nonce.disprove[i * 2 + 1].clone(), + ]; + let sigs = disprove_tx + .pre_sign(&committee_context, &sec_nonces, &agg_nonces) + .map_err(|e| anyhow::anyhow!("fail to pre-sign {}: {e}", disprove_tx.name()))?; + disprove_sigs.extend(sigs); + } + res.disprove = disprove_sigs; + } + + Ok(res) +} + +pub fn nonce_aggregation(pub_nonces: &Vec) -> AggNonce { + generate_aggregated_nonce(pub_nonces) +} + +pub fn nonces_aggregation(pub_nonces_vec: &[CommitteePubNonces]) -> Result { + fn aggregate_field(rows: &[CommitteePubNonces], get: F) -> Result> + where + F: Fn(&CommitteePubNonces) -> &Vec, + { + if rows.is_empty() { + return Ok(Vec::new()); + } + + let expected = get(&rows[0]).len(); + + for (idx, r) in rows.iter().enumerate() { + if get(r).len() != expected { + bail!("length mismatch on row {}: expected {}, got {}", idx, expected, get(r).len()) + } + } + + Ok((0..expected) + .map(|i| { + let column: Vec = rows.iter().map(|r| get(r)[i].clone()).collect(); + nonce_aggregation(&column) + }) + .collect()) + } + + Ok(CommitteeAggNonces { + take1: aggregate_field(pub_nonces_vec, |c| &c.take1)?, + take2: aggregate_field(pub_nonces_vec, |c| &c.take2)?, + challenge: aggregate_field(pub_nonces_vec, |c| &c.challenge)?, + watchtower_challenge_timeout: aggregate_field(pub_nonces_vec, |c| { + &c.watchtower_challenge_timeout + })?, + operator_challenge_nack: aggregate_field(pub_nonces_vec, |c| &c.operator_challenge_nack)?, + operator_commit_timeout: aggregate_field(pub_nonces_vec, |c| &c.operator_commit_timeout)?, + disprove: aggregate_field(pub_nonces_vec, |c| &c.disprove)?, + }) +} + +pub fn signature_aggregation( + partial_sigs: &Vec, + agg_nonces: &CommitteeAggNonces, + graph: &BitvmGcGraph, +) -> Result { + let context = graph.parameters.get_base_context(); + let verifier_num = graph.verifier_asserts.len(); + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + agg_nonces.validate_length(watchtower_num, verifier_num)?; + for r in partial_sigs { + r.validate_length(watchtower_num, verifier_num)?; + } + + let mut res: CommitteeSignatures = CommitteeSignatures::new_empty(); + + // take1 + let take1_agg_nonces = agg_nonces.take1.clone().try_into().unwrap(); + let mut take1_partial_sigs = [vec![], vec![]]; + partial_sigs.iter().for_each(|r| { + take1_partial_sigs[0].push(r.take1[0]); + take1_partial_sigs[1].push(r.take1[1]); + }); + match graph.take1.aggregate_pre_sigs(&context, &take1_partial_sigs, &take1_agg_nonces) { + Ok(v) => res.take1 = v.to_vec(), + Err(e) => bail!("fail to aggregate pre-sigs {}: {e}", graph.take1.name()), + }; + + // take2 + let take2_agg_nonces = agg_nonces.take2.clone().try_into().unwrap(); + let take2_partial_sigs = [partial_sigs.iter().map(|r| r.take2[0]).collect()]; + match graph.take2.aggregate_pre_sigs(&context, &take2_partial_sigs, &take2_agg_nonces) { + Ok(v) => res.take2 = v.to_vec(), + Err(e) => bail!("fail to aggregate pre-sigs {}: {e}", graph.take2.name()), + }; + + // challenge + let challenge_agg_nonces = agg_nonces.challenge.clone().try_into().unwrap(); + let challenge_partial_sigs = [partial_sigs.iter().map(|r| r.challenge[0]).collect()]; + match graph.challenge.aggregate_pre_sigs( + &context, + &challenge_partial_sigs, + &challenge_agg_nonces, + ) { + Ok(v) => res.challenge = v.to_vec(), + Err(e) => bail!("fail to aggregate pre-sigs {}: {e}", graph.challenge.name()), + }; + + // watchtower challenge timeout + let mut timeout_sigs = vec![]; + for (i, tx) in graph.watchtower_challenge_timeouts.iter().enumerate() { + let agg_nonces = [agg_nonces.watchtower_challenge_timeout[i].clone()]; + let partial_sigs = + [partial_sigs.iter().map(|r| r.watchtower_challenge_timeout[i]).collect()]; + let sigs = tx + .aggregate_pre_sigs(&context, &partial_sigs, &agg_nonces) + .map_err(|e| anyhow::anyhow!("fail to aggregate pre-sigs {}: {e}", tx.name()))?; + timeout_sigs.extend(sigs); + } + res.watchtower_challenge_timeout = timeout_sigs; + + // operator challenge nack + let mut nack_sigs = vec![]; + for (i, tx) in graph.operator_challenge_nacks.iter().enumerate() { + let agg_nonces = [ + agg_nonces.operator_challenge_nack[i * 2].clone(), + agg_nonces.operator_challenge_nack[i * 2 + 1].clone(), + ]; + let mut partial_sigs_by_input = [vec![], vec![]]; + partial_sigs.iter().for_each(|r| { + partial_sigs_by_input[0].push(r.operator_challenge_nack[i * 2]); + partial_sigs_by_input[1].push(r.operator_challenge_nack[i * 2 + 1]); + }); + let sigs = tx + .aggregate_pre_sigs(&context, &partial_sigs_by_input, &agg_nonces) + .map_err(|e| anyhow::anyhow!("fail to aggregate pre-sigs {}: {e}", tx.name()))?; + nack_sigs.extend(sigs); + } + res.operator_challenge_nack = nack_sigs; + + // operator commit timeout + let operator_commit_timeout_agg_nonces = + agg_nonces.operator_commit_timeout.clone().try_into().unwrap(); + let mut operator_commit_timeout_partial_sigs = [vec![], vec![]]; + partial_sigs.iter().for_each(|r| { + operator_commit_timeout_partial_sigs[0].push(r.operator_commit_timeout[0]); + operator_commit_timeout_partial_sigs[1].push(r.operator_commit_timeout[1]); + }); + match graph.operator_commit_timeout.aggregate_pre_sigs( + &context, + &operator_commit_timeout_partial_sigs, + &operator_commit_timeout_agg_nonces, + ) { + Ok(v) => res.operator_commit_timeout = v.to_vec(), + Err(e) => bail!("fail to aggregate pre-sigs {}: {e}", graph.operator_commit_timeout.name()), + }; + + // disprove + let mut disprove_sigs = vec![]; + for (i, disprove_tx) in graph.disproves.iter().enumerate() { + let _agg_nonces = + [agg_nonces.disprove[i * 2].clone(), agg_nonces.disprove[i * 2 + 1].clone()]; + let mut _partial_sigs = [vec![], vec![]]; + partial_sigs.iter().for_each(|r| { + _partial_sigs[0].push(r.disprove[i * 2]); + _partial_sigs[1].push(r.disprove[i * 2 + 1]); + }); + let sigs = disprove_tx.aggregate_pre_sigs(&context, &_partial_sigs, &_agg_nonces).map_err( + |e| anyhow::anyhow!("fail to aggregate pre-sigs {}: {e}", disprove_tx.name()), + )?; + disprove_sigs.extend(sigs); + } + res.disprove = disprove_sigs; + + Ok(res) +} + +fn taproot_script_spend_sighash( + tx: &Transaction, + input_index: usize, + prevouts: &[TxOut], + leaf_hash: TapLeafHash, + sighash_type: TapSighashType, +) -> Result { + let prevout = prevouts + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("missing prevout for input {input_index}"))?; + if sighash_type == TapSighashType::AllPlusAnyoneCanPay + || sighash_type == TapSighashType::SinglePlusAnyoneCanPay + || sighash_type == TapSighashType::NonePlusAnyoneCanPay + { + SighashCache::new(tx) + .taproot_script_spend_signature_hash( + input_index, + &Prevouts::One(input_index, prevout), + leaf_hash, + sighash_type, + ) + .map_err(|e| anyhow::anyhow!("failed to construct input {input_index} sighash: {e}")) + } else { + SighashCache::new(tx) + .taproot_script_spend_signature_hash( + input_index, + &Prevouts::All(prevouts), + leaf_hash, + sighash_type, + ) + .map_err(|e| anyhow::anyhow!("failed to construct input {input_index} sighash: {e}")) + } +} + +fn key_agg_context(committee_pubkeys: &[PublicKey]) -> Result { + let pubkeys: Vec = + committee_pubkeys.iter().map(|public_key| public_key.inner.into()).collect(); + KeyAggContext::new(pubkeys).map_err(|e| anyhow::anyhow!("invalid committee key set: {e}")) +} + +#[allow(clippy::too_many_arguments)] +pub fn verify_taproot_partial_signature( + committee_pubkeys: &[PublicKey], + committee_pubkey: &PublicKey, + pub_nonce: &PubNonce, + partial_sig: PartialSignature, + agg_nonce: &AggNonce, + tx: &Transaction, + input_index: usize, + prevouts: &[TxOut], + script: &Script, + sighash_type: TapSighashType, +) -> Result<()> { + let key_agg_ctx = key_agg_context(committee_pubkeys)?; + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); + let sighash = taproot_script_spend_sighash(tx, input_index, prevouts, leaf_hash, sighash_type)?; + verify_partial(&key_agg_ctx, partial_sig, agg_nonce, committee_pubkey.inner, pub_nonce, sighash) + .map_err(|e| anyhow::anyhow!("invalid partial signature from {committee_pubkey}: {e}")) +} + +pub fn verify_taproot_pre_signed_input( + tx: &T, + signing_pubkey: &XOnlyPublicKey, + input_index: usize, + sighash_type: TapSighashType, +) -> Result<()> { + let input = tx + .tx() + .input + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} is missing", tx.name()))?; + ensure!( + input.witness.len() == 3, + "{} input {input_index} must contain signature, script and control block", + tx.name() + ); + let signature_bytes = input + .witness + .nth(0) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} signature is missing", tx.name()))?; + let witness_script = input + .witness + .nth(1) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} script is missing", tx.name()))?; + let control_block_bytes = input.witness.nth(2).ok_or_else(|| { + anyhow::anyhow!("{} input {input_index} control block is missing", tx.name()) + })?; + let prevout = tx + .prev_outs() + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} prevout is missing", tx.name()))?; + let script = tx + .prev_scripts() + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} script is missing", tx.name()))?; + + ensure!( + witness_script == script.as_bytes(), + "{} input {input_index} witness script mismatch", + tx.name() + ); + ensure!( + prevout.script_pubkey.is_p2tr(), + "{} input {input_index} prevout is not P2TR", + tx.name() + ); + let output_key = + XOnlyPublicKey::from_slice(&prevout.script_pubkey.as_bytes()[2..]).map_err(|e| { + anyhow::anyhow!("{} input {input_index} output key is invalid: {e}", tx.name()) + })?; + let control_block = ControlBlock::decode(control_block_bytes).map_err(|e| { + anyhow::anyhow!("{} input {input_index} control block is invalid: {e}", tx.name()) + })?; + ensure!( + control_block.verify_taproot_commitment(SECP256K1, output_key, script), + "{} input {input_index} control block does not commit to the expected script", + tx.name() + ); + + let signature = TaprootSignature::from_slice(signature_bytes).map_err(|e| { + anyhow::anyhow!("{} input {input_index} signature is invalid: {e}", tx.name()) + })?; + ensure!( + signature.sighash_type == sighash_type, + "{} input {input_index} sighash type mismatch", + tx.name() + ); + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); + let sighash = taproot_script_spend_sighash( + tx.tx(), + input_index, + tx.prev_outs(), + leaf_hash, + sighash_type, + )?; + SECP256K1.verify_schnorr(&signature.signature, &Message::from(sighash), signing_pubkey).map_err( + |e| anyhow::anyhow!("{} input {input_index} signature verification failed: {e}", tx.name()), + ) +} + +#[allow(clippy::too_many_arguments)] +fn verify_pre_signed_input( + tx: &T, + committee_pubkeys: &[PublicKey], + committee_pubkey: &PublicKey, + pub_nonce: &PubNonce, + partial_sig: PartialSignature, + agg_nonce: &AggNonce, + input_index: usize, + sighash_type: TapSighashType, +) -> Result<()> { + verify_taproot_partial_signature( + committee_pubkeys, + committee_pubkey, + pub_nonce, + partial_sig, + agg_nonce, + tx.tx(), + input_index, + tx.prev_outs(), + &tx.prev_scripts()[input_index], + sighash_type, + ) + .map_err(|e| anyhow::anyhow!("fail to verify pre-signature {}[{input_index}]: {e}", tx.name())) +} + +pub fn verify_graph_committee_partial_sigs( + graph: &BitvmGcGraph, + committee_pubkeys: &[PublicKey], + committee_pubkey: &PublicKey, + pub_nonces: &CommitteePubNonces, + agg_nonces: &CommitteeAggNonces, + partial_sigs: &CommitteePartialSignatures, +) -> Result<()> { + ensure!( + committee_pubkeys.contains(committee_pubkey), + "committee pubkey {committee_pubkey} is not in the committee set" + ); + let verifier_num = graph.verifier_asserts.len(); + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + pub_nonces.validate_length(watchtower_num, verifier_num)?; + agg_nonces.validate_length(watchtower_num, verifier_num)?; + partial_sigs.validate_length(watchtower_num, verifier_num)?; + + verify_pre_signed_input( + &graph.take1, + committee_pubkeys, + committee_pubkey, + &pub_nonces.take1[0], + partial_sigs.take1[0], + &agg_nonces.take1[0], + 0, + TapSighashType::All, + )?; + verify_pre_signed_input( + &graph.take1, + committee_pubkeys, + committee_pubkey, + &pub_nonces.take1[1], + partial_sigs.take1[1], + &agg_nonces.take1[1], + 3, + TapSighashType::All, + )?; + verify_pre_signed_input( + &graph.take2, + committee_pubkeys, + committee_pubkey, + &pub_nonces.take2[0], + partial_sigs.take2[0], + &agg_nonces.take2[0], + 0, + TapSighashType::All, + )?; + verify_pre_signed_input( + &graph.challenge, + committee_pubkeys, + committee_pubkey, + &pub_nonces.challenge[0], + partial_sigs.challenge[0], + &agg_nonces.challenge[0], + 0, + TapSighashType::SinglePlusAnyoneCanPay, + )?; + + for (i, tx) in graph.watchtower_challenge_timeouts.iter().enumerate() { + verify_pre_signed_input( + tx, + committee_pubkeys, + committee_pubkey, + &pub_nonces.watchtower_challenge_timeout[i], + partial_sigs.watchtower_challenge_timeout[i], + &agg_nonces.watchtower_challenge_timeout[i], + 1, + TapSighashType::None, + )?; + } + + for (i, tx) in graph.operator_challenge_nacks.iter().enumerate() { + verify_pre_signed_input( + tx, + committee_pubkeys, + committee_pubkey, + &pub_nonces.operator_challenge_nack[i * 2], + partial_sigs.operator_challenge_nack[i * 2], + &agg_nonces.operator_challenge_nack[i * 2], + 0, + TapSighashType::All, + )?; + verify_pre_signed_input( + tx, + committee_pubkeys, + committee_pubkey, + &pub_nonces.operator_challenge_nack[i * 2 + 1], + partial_sigs.operator_challenge_nack[i * 2 + 1], + &agg_nonces.operator_challenge_nack[i * 2 + 1], + 1, + TapSighashType::All, + )?; + } + + verify_pre_signed_input( + &graph.operator_commit_timeout, + committee_pubkeys, + committee_pubkey, + &pub_nonces.operator_commit_timeout[0], + partial_sigs.operator_commit_timeout[0], + &agg_nonces.operator_commit_timeout[0], + 0, + TapSighashType::All, + )?; + verify_pre_signed_input( + &graph.operator_commit_timeout, + committee_pubkeys, + committee_pubkey, + &pub_nonces.operator_commit_timeout[1], + partial_sigs.operator_commit_timeout[1], + &agg_nonces.operator_commit_timeout[1], + 1, + TapSighashType::All, + )?; + + for (i, tx) in graph.disproves.iter().enumerate() { + verify_pre_signed_input( + tx, + committee_pubkeys, + committee_pubkey, + &pub_nonces.disprove[i * 2], + partial_sigs.disprove[i * 2], + &agg_nonces.disprove[i * 2], + 0, + TapSighashType::None, + )?; + verify_pre_signed_input( + tx, + committee_pubkeys, + committee_pubkey, + &pub_nonces.disprove[i * 2 + 1], + partial_sigs.disprove[i * 2 + 1], + &agg_nonces.disprove[i * 2 + 1], + 1, + TapSighashType::None, + )?; + } + + Ok(()) +} + +pub fn verify_graph_committee_pre_signatures(graph: &BitvmGcGraph) -> Result<()> { + ensure!(graph.committee_pre_signed(), "graph is not pre-signed by the committee"); + let (_, committee_pubkey) = + generate_n_of_n_public_key(&graph.parameters.instance_parameters.committee_pubkeys); + ensure!( + committee_pubkey == graph.parameters.instance_parameters.n_of_n_taproot_public_key(), + "graph committee aggregate public key mismatch" + ); + + verify_taproot_pre_signed_input(&graph.take1, &committee_pubkey, 0, TapSighashType::All)?; + verify_taproot_pre_signed_input(&graph.take1, &committee_pubkey, 3, TapSighashType::All)?; + verify_taproot_pre_signed_input(&graph.take2, &committee_pubkey, 0, TapSighashType::All)?; + verify_taproot_pre_signed_input( + &graph.challenge, + &committee_pubkey, + 0, + TapSighashType::SinglePlusAnyoneCanPay, + )?; + for tx in &graph.watchtower_challenge_timeouts { + verify_taproot_pre_signed_input(tx, &committee_pubkey, 1, TapSighashType::None)?; + } + for tx in &graph.operator_challenge_nacks { + verify_taproot_pre_signed_input(tx, &committee_pubkey, 0, TapSighashType::All)?; + verify_taproot_pre_signed_input(tx, &committee_pubkey, 1, TapSighashType::All)?; + } + verify_taproot_pre_signed_input( + &graph.operator_commit_timeout, + &committee_pubkey, + 0, + TapSighashType::All, + )?; + verify_taproot_pre_signed_input( + &graph.operator_commit_timeout, + &committee_pubkey, + 1, + TapSighashType::All, + )?; + for tx in &graph.disproves { + verify_taproot_pre_signed_input(tx, &committee_pubkey, 0, TapSighashType::None)?; + verify_taproot_pre_signed_input(tx, &committee_pubkey, 1, TapSighashType::None)?; + } + Ok(()) +} + +pub fn verify_pegin_confirm_partial_sig( + instance_parameters: &BitvmGcInstanceParameters, + committee_pubkeys: &[PublicKey], + committee_pubkey: &PublicKey, + pub_nonce: &PubNonce, + agg_nonce: &AggNonce, + partial_sig: PartialSignature, +) -> Result<()> { + ensure!( + committee_pubkeys.contains(committee_pubkey), + "committee pubkey {committee_pubkey} is not in the committee set" + ); + let pegin_confirm = instance_parameters.build_pegin_tx()?.1; + verify_pre_signed_input( + &pegin_confirm, + committee_pubkeys, + committee_pubkey, + pub_nonce, + partial_sig, + agg_nonce, + 0, + TapSighashType::All, + ) +} + +pub fn push_committee_pre_signatures( + graph: &mut BitvmGcGraph, + sigs: &CommitteeSignatures, +) -> Result<()> { + let verifier_num = graph.verifier_asserts.len(); + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + if graph.committee_pre_signed { + bail!("already pre-signed by committee".to_string()) + }; + sigs.validate_length(watchtower_num, verifier_num)?; + + let connector_0 = graph.parameters.connector_0(); + let connector_a = graph.connector_a(); + let connector_c = graph.connector_c(); + let connector_d = graph.connector_d(); + let connector_e = graph.connector_e(); + let connector_f = graph.connector_f(); + let ack_connectors = graph.ack_connectors(); + + // take1 + graph.take1.push_pre_sigs(&connector_0, &connector_c, sigs.take1.clone().try_into().unwrap()); + + // take2 + graph.take2.push_pre_sigs(&connector_0, sigs.take2.clone().try_into().unwrap()); + + // challenge + graph.challenge.push_pre_sigs(&connector_a, sigs.challenge.clone().try_into().unwrap()); + + // watchtower challenge timeout + for (i, tx) in graph.watchtower_challenge_timeouts.iter_mut().enumerate() { + tx.push_pre_sigs( + &ack_connectors[i], + sigs.watchtower_challenge_timeout[i..(i + 1)].try_into().unwrap(), + ); + } + + // operator challenge nack + for (i, tx) in graph.operator_challenge_nacks.iter_mut().enumerate() { + tx.push_pre_sigs( + &ack_connectors[i], + &connector_f, + sigs.operator_challenge_nack[i * 2..(i * 2 + 2)].try_into().unwrap(), + ); + } + + // operator commit timeout + graph.operator_commit_timeout.push_pre_sigs( + &connector_e, + &connector_f, + sigs.operator_commit_timeout.clone().try_into().unwrap(), + ); + + // disprove + let prover_connectors = graph.parameters.prover_connectors(); + for (i, disprove_tx) in graph.disproves.iter_mut().enumerate() { + disprove_tx.push_pre_sigs( + &prover_connectors[i], + &connector_d, + sigs.disprove[i * 2..(i * 2 + 2)].try_into().unwrap(), + ); + } + + graph.committee_pre_signed = true; + Ok(()) +} + +pub fn generate_nonce_from_seed( + seed: String, + graph_index: usize, + signer_keypair: Keypair, + watchtower_num: usize, + verifier_num: usize, +) -> (CommitteePubNonces, CommitteeSecNonces, CommitteeNonceSignatures) { + let graph_seed = hkdf_derive_bytes( + seed.as_bytes(), + COMMITTEE_NONCE_HKDF_SALT, + format!("graph/{graph_index}").as_bytes(), + 32, + ); + let mut pub_nonces = CommitteePubNonces::new_empty(); + let mut sec_nonces = CommitteeSecNonces::new_empty(); + let mut nonce_sigs = CommitteeNonceSignatures::new_empty(); + let mut index = 0; + { + // take1 + for _ in 0..take1_pre_sign_num() { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.take1.push(pub_nonce); + sec_nonces.take1.push(sec_nonce); + nonce_sigs.take1.push(nonce_sig); + index += 1; + } + } + { + // take2 + for _ in 0..take2_pre_sign_num() { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.take2.push(pub_nonce); + sec_nonces.take2.push(sec_nonce); + nonce_sigs.take2.push(nonce_sig); + index += 1; + } + } + { + // challenge + for _ in 0..challenge_pre_sign_num() { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.challenge.push(pub_nonce); + sec_nonces.challenge.push(sec_nonce); + nonce_sigs.challenge.push(nonce_sig); + index += 1; + } + } + { + // watchtower challenge timeout + for _ in 0..watchtower_challenge_timeout_pre_sign_num(watchtower_num) { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.watchtower_challenge_timeout.push(pub_nonce); + sec_nonces.watchtower_challenge_timeout.push(sec_nonce); + nonce_sigs.watchtower_challenge_timeout.push(nonce_sig); + index += 1; + } + } + { + // operator challenge nack + for _ in 0..operator_challenge_nack_pre_sign_num(watchtower_num) { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.operator_challenge_nack.push(pub_nonce); + sec_nonces.operator_challenge_nack.push(sec_nonce); + nonce_sigs.operator_challenge_nack.push(nonce_sig); + index += 1; + } + } + { + // operator commit timeout + for _ in 0..operator_commit_timeout_pre_sign_num() { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.operator_commit_timeout.push(pub_nonce); + sec_nonces.operator_commit_timeout.push(sec_nonce); + nonce_sigs.operator_commit_timeout.push(nonce_sig); + index += 1; + } + } + { + // disprove + for _ in 0..disprove_pre_sign_num(verifier_num) { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.disprove.push(pub_nonce); + sec_nonces.disprove.push(sec_nonce); + nonce_sigs.disprove.push(nonce_sig); + index += 1; + } + } + (pub_nonces, sec_nonces, nonce_sigs) +} + +pub fn verify_nonce_signatures( + pubkey: &XOnlyPublicKey, + pub_nonces: &CommitteePubNonces, + nonce_sigs: &CommitteeNonceSignatures, + watchtower_num: usize, + verifier_num: usize, +) -> Result { + pub_nonces.validate_length(watchtower_num, verifier_num)?; + nonce_sigs.validate_length(watchtower_num, verifier_num)?; + + fn verify_vec(pubkey: &XOnlyPublicKey, nonces: &[PubNonce], sigs: &[SchnorrSignature]) -> bool { + if nonces.len() != sigs.len() { + return false; + } + nonces.iter().zip(sigs.iter()).all(|(nonce, sig)| verify_public_nonce(sig, nonce, pubkey)) + } + + Ok(verify_vec(pubkey, &pub_nonces.take1, &nonce_sigs.take1) + && verify_vec(pubkey, &pub_nonces.take2, &nonce_sigs.take2) + && verify_vec(pubkey, &pub_nonces.challenge, &nonce_sigs.challenge) + && verify_vec( + pubkey, + &pub_nonces.watchtower_challenge_timeout, + &nonce_sigs.watchtower_challenge_timeout, + ) + && verify_vec( + pubkey, + &pub_nonces.operator_challenge_nack, + &nonce_sigs.operator_challenge_nack, + ) + && verify_vec( + pubkey, + &pub_nonces.operator_commit_timeout, + &nonce_sigs.operator_commit_timeout, + ) + && verify_vec(pubkey, &pub_nonces.disprove, &nonce_sigs.disprove)) +} + +pub(crate) fn generate_nonce( + signer_keypair: Keypair, + seed: &[u8], + index: usize, +) -> (SecNonce, PubNonce, SchnorrSignature) { + let nonce_seed = + hkdf_derive_bytes(seed, COMMITTEE_NONCE_HKDF_SALT, format!("nonce/{index}").as_bytes(), 32); + let nonce_seed: [u8; 32] = + nonce_seed.try_into().expect("hkdf output length is fixed to 32 bytes"); + let sec_nonce = SecNonce::build(nonce_seed).build(); + let pub_nonce = sec_nonce.public_nonce(); + let nonce_signature = signer_keypair.sign_schnorr(get_nonce_message(&pub_nonce)); + (sec_nonce, pub_nonce, nonce_signature) +} diff --git a/crates/bitvm2-ga/src/challenger/mod.rs b/crates/bitvm-gc/src/committee/mod.rs similarity index 100% rename from crates/bitvm2-ga/src/challenger/mod.rs rename to crates/bitvm-gc/src/committee/mod.rs diff --git a/crates/bitvm2-ga/src/keys.rs b/crates/bitvm-gc/src/keys.rs similarity index 87% rename from crates/bitvm2-ga/src/keys.rs rename to crates/bitvm-gc/src/keys.rs index 6aff51640..6da15bdd6 100644 --- a/crates/bitvm2-ga/src/keys.rs +++ b/crates/bitvm-gc/src/keys.rs @@ -2,11 +2,7 @@ use crate::committee::{ CommitteeNonceSignatures, CommitteePubNonces, CommitteeSecNonces, generate_nonce, generate_nonce_from_seed, }; - -use super::{ - operator::generate_wots_keys, - types::{OperatorWotsPublicKeys, OperatorWotsSecretKeys}, -}; +use crate::operator::{generate_assert_wots_key, generate_commit_pubin_wots_key}; use anyhow::{Context, bail}; use bitcoin::{ Network, PublicKey, @@ -18,6 +14,10 @@ use chacha20poly1305::{ ChaCha20Poly1305, KeyInit, Nonce, aead::{Aead, Payload}, }; +use goat::assert_scripts::{ + OperatorAssertPublicKey, OperatorAssertSecretKey, OperatorCommitPubinPublicKey, + OperatorCommitPubinSecretKey, +}; use hex::{decode as hex_decode, encode as hex_encode}; use hkdf::Hkdf; use musig2::{PubNonce, SecNonce}; @@ -30,17 +30,24 @@ use std::time::{SystemTime, UNIX_EPOCH}; use std::{fs, io::Write, path::Path}; use uuid::Uuid; -const HKDF_SALT: &[u8] = b"bitvm2/keys/v1"; +pub type OperatorAssertWotsSecretKey = OperatorAssertSecretKey; +pub type OperatorAssertWotsPublicKey = OperatorAssertPublicKey; +pub type OperatorAssertWotsKeypair = (OperatorAssertWotsSecretKey, OperatorAssertWotsPublicKey); +pub type OperatorCommitPubinWotsSecretKey = OperatorCommitPubinSecretKey; +pub type OperatorCommitPubinWotsPublicKey = OperatorCommitPubinPublicKey; +pub type OperatorCommitPubinWotsKeypair = + (OperatorCommitPubinWotsSecretKey, OperatorCommitPubinWotsPublicKey); + +const HKDF_SALT: &[u8] = b"bitvm-gc/keys/v1"; const BITVM_BIP32_ROOT_DOMAIN: &[u8] = b"bitvm_bip32_root"; -const PURPOSE_BITVM2_DERIVATION: u32 = 2345; +const PURPOSE_BITVM_GC_DERIVATION: u32 = 2345; const ROLE_COMMITTEE: u32 = 0; const ROLE_OPERATOR: u32 = 1; -const ROLE_CHALLENGER: u32 = 2; +// const ROLE_VERIFIER: u32 = 2; const KEY_KIND_COMMITTEE_ENVELOPE: u32 = 0; const KEY_KIND_OPERATOR_NONCE: u32 = 1; -const KEY_KIND_CHALLENGER_DISPROVE: u32 = 2; const COMMITTEE_ENVELOPE_VERSION: u8 = 1; @@ -95,7 +102,8 @@ fn operator_nonce_derivation_path(nonce: u64) -> DerivationPath { ]; let mut path = vec![ - ChildNumber::from_hardened_idx(PURPOSE_BITVM2_DERIVATION).expect("constant index is valid"), + ChildNumber::from_hardened_idx(PURPOSE_BITVM_GC_DERIVATION) + .expect("constant index is valid"), ChildNumber::from_hardened_idx(ROLE_OPERATOR).expect("constant index is valid"), ChildNumber::from_hardened_idx(KEY_KIND_OPERATOR_NONCE).expect("constant index is valid"), ]; @@ -107,16 +115,6 @@ fn operator_nonce_derivation_path(nonce: u64) -> DerivationPath { DerivationPath::from(path) } -// Path layout: m / purpose' / role' / key_kind' -fn challenger_disprove_derivation_path() -> DerivationPath { - DerivationPath::from(vec![ - ChildNumber::from_hardened_idx(PURPOSE_BITVM2_DERIVATION).expect("constant index is valid"), - ChildNumber::from_hardened_idx(ROLE_CHALLENGER).expect("constant index is valid"), - ChildNumber::from_hardened_idx(KEY_KIND_CHALLENGER_DISPROVE) - .expect("constant index is valid"), - ]) -} - // Path layout: m / purpose' / role_committee' / key_kind' / iid_0' / ... / iid_7' fn committee_kek_derivation_path(instance_id: Uuid) -> DerivationPath { let instance = instance_id.as_u128(); @@ -132,7 +130,8 @@ fn committee_kek_derivation_path(instance_id: Uuid) -> DerivationPath { ]; let mut path = vec![ - ChildNumber::from_hardened_idx(PURPOSE_BITVM2_DERIVATION).expect("constant index is valid"), + ChildNumber::from_hardened_idx(PURPOSE_BITVM_GC_DERIVATION) + .expect("constant index is valid"), ChildNumber::from_hardened_idx(ROLE_COMMITTEE).expect("constant index is valid"), ChildNumber::from_hardened_idx(KEY_KIND_COMMITTEE_ENVELOPE) .expect("constant index is valid"), @@ -339,18 +338,20 @@ impl CommitteeMasterKey { Ok(()) } - pub fn nonces_for_graph_with_keypair( + pub fn nonces_for_graph_job_with_keypair( &self, instance_id: Uuid, graph_id: Uuid, + graph_parameters_hash: [u8; 32], watchtower_num: usize, - assert_commit_num: usize, + verifier_num: usize, signer_keypair: Keypair, ) -> (CommitteePubNonces, CommitteeSecNonces, CommitteeNonceSignatures) { let domain = [ - b"committee_bitvm_graph_nonces".to_vec(), + b"committee_bitvm_graph_nonces_v2".to_vec(), instance_id.as_bytes().to_vec(), graph_id.as_bytes().to_vec(), + graph_parameters_hash.to_vec(), ] .concat(); let nonce_seed = derive_secret(&signer_keypair, &domain); @@ -359,17 +360,22 @@ impl CommitteeMasterKey { graph_id.as_u128() as usize, signer_keypair, watchtower_num, - assert_commit_num, + verifier_num, ) } - pub fn nonce_for_instance_with_keypair( + pub fn nonce_for_instance_job_with_keypair( &self, instance_id: Uuid, + instance_parameters_hash: [u8; 32], signer_keypair: Keypair, ) -> (SecNonce, PubNonce, SchnorrSignature) { - let domain = - [b"committee_bitvm_instance_nonce".to_vec(), instance_id.as_bytes().to_vec()].concat(); + let domain = [ + b"committee_bitvm_instance_nonce_v2".to_vec(), + instance_id.as_bytes().to_vec(), + instance_parameters_hash.to_vec(), + ] + .concat(); let nonce_seed = derive_secret(&signer_keypair, &domain); generate_nonce(signer_keypair, nonce_seed.as_bytes(), 0) } @@ -391,13 +397,19 @@ impl OperatorMasterKey { .expect("valid derivation path should derive child key"); Keypair::from_secret_key(SECP256K1, &child.private_key) } - pub fn wots_keypair_for_graph( + pub fn assert_wots_keypair_for_graph(&self, graph_id: Uuid) -> OperatorAssertWotsKeypair { + let domain = [b"operator_bitvm_wots_key".to_vec(), graph_id.as_bytes().to_vec()].concat(); + let key_seed = derive_secret(&self.0, &domain); + generate_assert_wots_key(&key_seed) + } + pub fn commit_pubin_wots_keypair_for_graph( &self, graph_id: Uuid, - ) -> (OperatorWotsSecretKeys, OperatorWotsPublicKeys) { - let domain = [b"operator_bitvm_wots_key".to_vec(), graph_id.as_bytes().to_vec()].concat(); - let wot_seed = derive_secret(&self.0, &domain); - generate_wots_keys(&wot_seed) + ) -> OperatorCommitPubinWotsKeypair { + let domain = + [b"operator_bitvm_pubin_wots_key".to_vec(), graph_id.as_bytes().to_vec()].concat(); + let key_seed = derive_secret(&self.0, &domain); + generate_commit_pubin_wots_key(&key_seed) } pub fn preimage_for_graph(&self, graph_id: Uuid, index: usize) -> Vec { let domain = [ @@ -410,22 +422,14 @@ impl OperatorMasterKey { } } -pub struct ChallengerMasterKey(Keypair); -impl ChallengerMasterKey { +pub struct VerifierMasterKey(Keypair); +impl VerifierMasterKey { pub fn new(inner: Keypair) -> Self { - ChallengerMasterKey(inner) + VerifierMasterKey(inner) } pub fn master_keypair(&self) -> Keypair { NodeMasterKey(self.0).master_keypair() } - pub fn keypair_for_nst_disprove(&self) -> Keypair { - let root = derive_bip32_root(&self.0); - let path = challenger_disprove_derivation_path(); - let child = root - .derive_priv(SECP256K1, &path) - .expect("valid derivation path should derive child key"); - Keypair::from_secret_key(SECP256K1, &child.private_key) - } } pub struct WatchtowerMasterKey(Keypair); @@ -452,7 +456,7 @@ mod tests { fn test_envelope_path(instance_id: Uuid) -> PathBuf { let mut path = std::env::temp_dir(); - path.push(format!("bitvm2-committee-key-{instance_id}.json")); + path.push(format!("bitvm-gc-committee-key-{instance_id}.json")); path } @@ -582,6 +586,23 @@ mod tests { assert_ne!(pub_a1, pub_b, "different nonce should derive different keypairs"); } + #[test] + fn operator_assert_wots_keypair_for_graph_is_deterministic_and_graph_scoped() { + let master = OperatorMasterKey::new(test_master_keypair("seed:test-operator-master")); + let graph_a = Uuid::new_v4(); + let graph_b = Uuid::new_v4(); + + let keypair_a1 = master.assert_wots_keypair_for_graph(graph_a); + let keypair_a2 = master.assert_wots_keypair_for_graph(graph_a); + let keypair_b = master.assert_wots_keypair_for_graph(graph_b); + + assert_eq!(keypair_a1.1, keypair_a2.1, "same graph should derive same WOTS pubkey"); + assert_ne!( + keypair_a1.1, keypair_b.1, + "different graphs should derive different WOTS pubkeys" + ); + } + #[test] fn operator_nonce_derivation_path_has_expected_bip32_layout() { let nonce: u64 = 0x1122_3344_5566_7788; @@ -589,7 +610,10 @@ mod tests { let children: Vec = path.into_iter().cloned().collect(); assert_eq!(children.len(), 7, "path should be purpose/role/key_kind + 4 segments"); - assert_eq!(children[0], ChildNumber::from_hardened_idx(PURPOSE_BITVM2_DERIVATION).unwrap()); + assert_eq!( + children[0], + ChildNumber::from_hardened_idx(PURPOSE_BITVM_GC_DERIVATION).unwrap() + ); assert_eq!(children[1], ChildNumber::from_hardened_idx(ROLE_OPERATOR).unwrap()); assert_eq!(children[2], ChildNumber::from_hardened_idx(KEY_KIND_OPERATOR_NONCE).unwrap()); assert_eq!(children[3], ChildNumber::from_hardened_idx(0x1122).unwrap()); @@ -613,16 +637,4 @@ mod tests { assert_ne!(kek_a1, kek_b, "different instances should derive different kek"); assert_ne!(path_a1, path_b, "different instances should derive different path"); } - - #[test] - fn challenger_nst_disprove_keypair_is_deterministic() { - let master = ChallengerMasterKey::new(test_master_keypair("seed:test-challenger-master")); - - let keypair_1 = master.keypair_for_nst_disprove(); - let keypair_2 = master.keypair_for_nst_disprove(); - - let pub_1: PublicKey = keypair_1.public_key().into(); - let pub_2: PublicKey = keypair_2.public_key().into(); - assert_eq!(pub_1, pub_2, "challenger disprove keypair should be stable"); - } } diff --git a/crates/bitvm2-ga/src/lib.rs b/crates/bitvm-gc/src/lib.rs similarity index 69% rename from crates/bitvm2-ga/src/lib.rs rename to crates/bitvm-gc/src/lib.rs index faa1e805b..f0383511a 100644 --- a/crates/bitvm2-ga/src/lib.rs +++ b/crates/bitvm-gc/src/lib.rs @@ -1,13 +1,13 @@ -pub mod challenger; pub mod committee; pub mod operator; +pub mod verifier; pub mod watchtower; pub mod actors; +pub mod babe_adapter; pub mod keys; pub mod pegin; +pub mod timelocks; pub mod types; pub use goat::*; - -mod tests; diff --git a/crates/bitvm-gc/src/operator/api.rs b/crates/bitvm-gc/src/operator/api.rs new file mode 100644 index 000000000..563f080ad --- /dev/null +++ b/crates/bitvm-gc/src/operator/api.rs @@ -0,0 +1,656 @@ +use anyhow::{Result, bail}; +use bitcoin::{Address, Amount, TapSighashType, Transaction, TxIn, key::Keypair}; +use bitcoin::{Network, PublicKey, Witness, XOnlyPublicKey}; +use goat::assert_scripts::{ + OperatorAssertPublicKey, OperatorAssertSecretKey, OperatorCommitPubinPublicKey, + OperatorCommitPubinSecretKey, +}; +use goat::constants::TimelockConfig; +use goat::transactions::assert::{ + DisproveTransaction, OperatorAssertTransaction, VerifierAssertTransaction, wrongly_challenged, +}; +use goat::transactions::base::DUST_AMOUNT; +use goat::transactions::challenge::ChallengeTransaction; +use goat::transactions::kickoff::KickoffTransaction; +use goat::transactions::pre_signed::PreSignedTransaction; +use goat::transactions::prekickoff::{ + ChallengeIncompleteKickoffTransaction, ForceSkipKickoffTransaction, PrekickoffTransaction, + QuickChallengeTransaction, operator_skip_kickoff, +}; +use goat::transactions::take1::Take1Transaction; +use goat::transactions::take2::Take2Transaction; +use goat::transactions::watchtower_challenge::{ + OperatorChallengeNackTransaction, OperatorCommitTimeoutTransaction, + WatchtowerChallengeInitTransaction, WatchtowerChallengeTimeoutTransaction, + operator_challenge_ack, operator_commit_pubin, +}; +use goat::wots::{Wots, Wots96}; + +use crate::committee::verify_taproot_pre_signed_input; +use crate::keys::hkdf_derive_bytes; +use crate::timelocks::{ + default_timelock_config, take1_timelock_blocks, take2_timelock_blocks, validate_timelock_config, +}; +use crate::types::{BitvmGcGraph, BitvmGcGraphParameters}; + +const OPERATOR_ASSERT_WOTS_HKDF_SALT: &[u8] = b"bitvm-gc/operator-wots/v2"; +const OPERATOR_COMMIT_PUBIN_WOTS_HKDF_SALT: &[u8] = b"bitvm-gc/operator-commit-pubin-wots/v2"; + +#[allow(deprecated)] +pub fn generate_assert_wots_key(seed: &str) -> (OperatorAssertSecretKey, OperatorAssertPublicKey) { + let sec_str = hex::encode(hkdf_derive_bytes( + seed.as_bytes(), + OPERATOR_ASSERT_WOTS_HKDF_SALT, + b"wots96/0", + 96, + )); + let secret = Wots96::secret_from_str(&sec_str); + let public = Wots96::generate_public_key(&secret); + (secret, public) +} + +#[allow(deprecated)] +pub fn generate_commit_pubin_wots_key( + seed: &str, +) -> (OperatorCommitPubinSecretKey, OperatorCommitPubinPublicKey) { + let sec_str = hex::encode(hkdf_derive_bytes( + seed.as_bytes(), + OPERATOR_COMMIT_PUBIN_WOTS_HKDF_SALT, + b"wots96/0", + 96, + )); + let secret = Wots96::secret_from_str(&sec_str); + let public = Wots96::generate_public_key(&secret); + (secret, public) +} + +pub fn operator_presig_num() -> usize { + 6 +} + +pub fn generate_bitvm_graph(params: BitvmGcGraphParameters) -> Result { + validate_timelock_config(params.network(), ¶ms.timelock_config)?; + + let watchtower_num = params.watchtower_pubkeys.len(); + let verifier_num = params.gc_data.len(); + if params.watchtower_ack_hashlocks.len() != watchtower_num { + bail!( + "watchtower ack hashlock count {} does not match watchtower count {watchtower_num}", + params.watchtower_ack_hashlocks.len() + ); + } + + let (_, pegin, _) = params.instance_parameters.build_pegin_tx()?; + let connector_0_input = pegin + .connector_0_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-0 input: {e}"))?; + + let cur_prekickoff_connector = params.prekickoff_connector(); + let next_force_skip_connector = params.force_skip_connector(); + let next_kickoff_connector = params.kickoff_connector(); + let next_prekickoff_connector = params.prekickoff_connector(); + let cur_prekickoff = params.prekickoff_parameters.cur_prekickoff_txn.clone(); + let cur_prekickoff_connector_input = cur_prekickoff + .prekickoff_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get current pre-kickoff connector input: {e}"))?; + let next_prekickoff = PrekickoffTransaction::new_for_validation( + &cur_prekickoff_connector, + &next_force_skip_connector, + &next_kickoff_connector, + &next_prekickoff_connector, + cur_prekickoff_connector_input, + params.prekickoff_parameters.replenish_fee_inputs.clone(), + params.prekickoff_parameters.replenish_fee_prev_outs.clone(), + params.prekickoff_parameters.fee_amount, + watchtower_num, + verifier_num, + ) + .map_err(|e| anyhow::anyhow!("failed to create pre-kickoff txn: {e}"))?; + let next_force_skip_connector_input = next_prekickoff + .force_skip_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get next force-skip connector input: {e}"))?; + let next_prekickoff_connector_input = next_prekickoff + .prekickoff_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get next pre-kickoff connector input: {e}"))?; + + // kickoff + let kickoff_connector_input = cur_prekickoff + .kickoff_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get kickoff connector input: {e}"))?; + let kickoff_connector = params.kickoff_connector(); + let connector_a = params.connector_a(); + let connector_b = params.connector_b(); + let connector_c = params.connector_c(); + let guardian_connector = params.guardian_connector(); + let kickoff = KickoffTransaction::new_for_validation( + &kickoff_connector, + &connector_a, + &connector_b, + &connector_c, + &guardian_connector, + &kickoff_connector_input, + watchtower_num, + verifier_num, + ) + .map_err(|e| anyhow::anyhow!("failed to create kickoff txn: {e}"))?; + let connector_a_input = kickoff + .connector_a_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-a input: {e}"))?; + let connector_b_input = kickoff + .connector_b_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-b input: {e}"))?; + let connector_c_input = kickoff + .connector_c_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-c input: {e}"))?; + let guardian_connector_input = kickoff + .guardian_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get guardian connector input: {e}"))?; + + // prekickoff challenge + let force_skip_kickoff = ForceSkipKickoffTransaction::new_for_validation( + &kickoff_connector, + &next_force_skip_connector, + kickoff_connector_input, + next_force_skip_connector_input.clone(), + ); + let quick_challenge = QuickChallengeTransaction::new_for_validation( + &guardian_connector, + &next_force_skip_connector, + guardian_connector_input.clone(), + next_force_skip_connector_input, + ); + let challenge_incomplete_kickoff = ChallengeIncompleteKickoffTransaction::new_for_validation( + &guardian_connector, + &next_prekickoff_connector, + guardian_connector_input.clone(), + next_prekickoff_connector_input, + ); + + // take-1 + let connector_0 = params.connector_0(); + let take1 = Take1Transaction::new_for_validation( + &connector_0, + &connector_a, + &connector_b, + &connector_c, + &guardian_connector, + connector_0_input.clone(), + connector_a_input.clone(), + connector_b_input.clone(), + connector_c_input.clone(), + guardian_connector_input.clone(), + ¶ms.operator_receive_address, + ) + .map_err(|e| anyhow::anyhow!("failed to create take-1 txn: {e}"))?; + + // challenge + let challenge = ChallengeTransaction::new_for_validation( + &connector_a, + connector_a_input, + params.challenge_amount, + ¶ms.operator_receive_address, + ); + + // watchtower-challenge + let connector_e = params.connector_e(); + let connector_f = params.connector_f(); + let watchtower_challenge_connectors = params.watchtower_challenge_connectors(); + let ack_connectors = params.ack_connectors(); + let watchtower_challenge_init = WatchtowerChallengeInitTransaction::new_for_validation( + &connector_b, + &connector_e, + &connector_f, + &watchtower_challenge_connectors, + &ack_connectors, + connector_b_input, + ) + .map_err(|e| anyhow::anyhow!("failed to create watchtower-challenge-init txn: {e}"))?; + let connector_e_input = watchtower_challenge_init + .connector_e_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-e input: {e}"))?; + let connector_f_input = watchtower_challenge_init + .connector_f_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-f input: {e}"))?; + let mut watchtower_challenge_timeouts = Vec::with_capacity(watchtower_num); + let mut operator_challenge_nacks = Vec::with_capacity(watchtower_num); + for (i, (watchtower_connector, ack_connector)) in + watchtower_challenge_connectors.iter().zip(ack_connectors.iter()).enumerate() + { + let watchtower_input = watchtower_challenge_init + .watchtower_connector_input(i) + .map_err(|e| anyhow::anyhow!("failed to get watchtower connector input {i}: {e}"))?; + let ack_input = watchtower_challenge_init + .ack_connector_input(i) + .map_err(|e| anyhow::anyhow!("failed to get ack connector input {i}: {e}"))?; + watchtower_challenge_timeouts.push( + WatchtowerChallengeTimeoutTransaction::new_for_validation( + watchtower_connector, + ack_connector, + watchtower_input, + ack_input.clone(), + ), + ); + operator_challenge_nacks.push(OperatorChallengeNackTransaction::new_for_validation( + ack_connector, + &connector_f, + ack_input, + connector_f_input.clone(), + )); + } + let operator_commit_timeout = OperatorCommitTimeoutTransaction::new_for_validation( + &connector_e, + &connector_f, + connector_e_input.clone(), + connector_f_input.clone(), + ); + + // prover-assert + let connector_d = params.connector_d(); + let verifier_connectors = params.verifier_connectors(); + let operator_assert = OperatorAssertTransaction::new_for_validation( + &connector_c, + &verifier_connectors, + &connector_d, + connector_c_input, + ) + .map_err(|e| anyhow::anyhow!("failed to create operator assert txn: {e}"))?; + let connector_d_input = operator_assert + .connector_d_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-d input: {e}"))?; + + // verifier-asserts and disproves + let mut verifier_asserts = Vec::with_capacity(verifier_num); + let mut disproves = Vec::with_capacity(verifier_num); + for (i, verifier_connector) in verifier_connectors.iter().enumerate() { + let verifier_input = operator_assert + .verifier_connector_input(i) + .map_err(|e| anyhow::anyhow!("failed to get verifier connector input {i}: {e}"))?; + let prover_connector = params.prover_connector(i)?; + let verifier_assert = VerifierAssertTransaction::new_for_validation( + verifier_connector, + &prover_connector, + verifier_input, + ) + .map_err(|e| anyhow::anyhow!("failed to create verifier assert txn {i}: {e}"))?; + let prover_input = verifier_assert + .prover_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get prover connector input {i}: {e}"))?; + let disprove = DisproveTransaction::new_for_validation( + &prover_connector, + &connector_d, + prover_input, + connector_d_input.clone(), + Vec::new(), + ) + .map_err(|e| anyhow::anyhow!("failed to create disprove txn {i}: {e}"))?; + verifier_asserts.push(verifier_assert); + disproves.push(disprove); + } + + // take-2 + let take2 = Take2Transaction::new_for_validation( + &connector_0, + &connector_d, + &connector_f, + &guardian_connector, + connector_0_input, + connector_d_input, + connector_f_input, + guardian_connector_input, + ¶ms.operator_receive_address, + ) + .map_err(|e| anyhow::anyhow!("failed to create take-2 txn: {e}"))?; + + Ok(BitvmGcGraph { + operator_pre_signed: false, + committee_pre_signed: false, + parameters: params, + cur_prekickoff, + next_prekickoff, + force_skip_kickoff, + quick_challenge, + challenge_incomplete_kickoff, + pegin, + kickoff, + take1, + challenge, + watchtower_challenge_init, + watchtower_challenge_timeouts, + operator_challenge_nacks, + operator_commit_timeout, + operator_assert, + verifier_asserts, + disproves, + take2, + }) +} + +pub fn operator_pre_sign( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, +) -> Result> { + let keypair_pubkey = PublicKey::from(operator_keypair.public_key()); + if keypair_pubkey != graph.parameters.operator_pubkey { + bail!("operator keypair does not match graph operator pubkey".to_string()) + }; + + let mut wits = vec![]; + let context = graph.parameters.get_operator_context(operator_keypair)?; + + // presign force_skip_kickoff + let kickoff_connector = graph.parameters.kickoff_connector(); + let next_force_skip_connector = graph.parameters.force_skip_connector(); + graph.force_skip_kickoff.pre_sign_and_push( + &context, + &kickoff_connector, + &next_force_skip_connector, + ); + wits.push(graph.force_skip_kickoff.tx().input[0].witness.clone()); + wits.push(graph.force_skip_kickoff.tx().input[1].witness.clone()); + + // presign quick_challenge + let guardian_connector = graph.guardian_connector(); + graph.quick_challenge.pre_sign_and_push( + &context, + &guardian_connector, + &next_force_skip_connector, + ); + wits.push(graph.quick_challenge.tx().input[0].witness.clone()); + wits.push(graph.quick_challenge.tx().input[1].witness.clone()); + + // presign challenge_incomplete_kickoff + let next_prekickoff_connector = graph.parameters.prekickoff_connector(); + graph.challenge_incomplete_kickoff.pre_sign_and_push( + &context, + &guardian_connector, + &next_prekickoff_connector, + ); + wits.push(graph.challenge_incomplete_kickoff.tx().input[0].witness.clone()); + wits.push(graph.challenge_incomplete_kickoff.tx().input[1].witness.clone()); + + graph.operator_pre_signed = true; + Ok(wits) +} + +pub fn push_operator_pre_signature( + graph: &mut BitvmGcGraph, + signed_witness: &[Witness], +) -> Result<()> { + if graph.operator_pre_signed { + bail!("already pre-signed by operator".to_string()) + }; + if signed_witness.len() != operator_presig_num() { + bail!("invalid number of pre-signatures".to_string()) + }; + + graph.force_skip_kickoff.tx_mut().input[0].witness = signed_witness[0].clone(); + graph.force_skip_kickoff.tx_mut().input[1].witness = signed_witness[1].clone(); + graph.quick_challenge.tx_mut().input[0].witness = signed_witness[2].clone(); + graph.quick_challenge.tx_mut().input[1].witness = signed_witness[3].clone(); + graph.challenge_incomplete_kickoff.tx_mut().input[0].witness = signed_witness[4].clone(); + graph.challenge_incomplete_kickoff.tx_mut().input[1].witness = signed_witness[5].clone(); + + graph.operator_pre_signed = true; + Ok(()) +} + +pub fn verify_graph_operator_pre_signatures(graph: &BitvmGcGraph) -> Result<()> { + if !graph.operator_pre_signed() { + bail!("graph is not pre-signed by the operator"); + } + let operator_pubkey = XOnlyPublicKey::from(graph.parameters.operator_pubkey); + + verify_taproot_pre_signed_input( + &graph.force_skip_kickoff, + &operator_pubkey, + 0, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.force_skip_kickoff, + &operator_pubkey, + 1, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.quick_challenge, + &operator_pubkey, + 0, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.quick_challenge, + &operator_pubkey, + 1, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.challenge_incomplete_kickoff, + &operator_pubkey, + 0, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.challenge_incomplete_kickoff, + &operator_pubkey, + 1, + TapSighashType::None, + )?; + Ok(()) +} + +/// remember to sign replensish inputs (if any) after this +pub fn operator_sign_prekickoff_input_0( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, +) -> Result { + let operator_context = graph.parameters.get_operator_context(operator_keypair)?; + let prev_prekickoff_connector = graph.parameters.prekickoff_connector(); + graph.cur_prekickoff.sign_input_0(&operator_context, &prev_prekickoff_connector); + Ok(graph.cur_prekickoff.tx().clone()) +} + +pub fn operator_sign_skip_kickoff( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, + operator_receive_address: Address, + fee_rate: f64, +) -> Result> { + let operator_context = graph.parameters.get_operator_context(operator_keypair)?; + let kickoff_connector = graph.parameters.kickoff_connector(); + let kickoff_connector_input = graph + .cur_prekickoff + .kickoff_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get kickoff connector input: {e}"))?; + // create a sample tx to estimate fee + let sample_tx = operator_skip_kickoff( + &operator_context, + &kickoff_connector, + kickoff_connector_input.clone(), + Amount::ZERO, + operator_receive_address.clone(), + ) + .map_err(|e| anyhow::anyhow!("failed to create sample skip-kickoff txn: {e}"))?; + + let fee_amount = + Amount::from_sat((sample_tx.weight().to_vbytes_ceil() as f64 * fee_rate).ceil() as u64); + if fee_amount + Amount::from_sat(DUST_AMOUNT) >= kickoff_connector_input.amount { + // if fee_amount > input_amount - dust_amount, skip-kickoff tx is meaningless + return Ok(None); + } + match operator_skip_kickoff( + &operator_context, + &kickoff_connector, + kickoff_connector_input, + fee_amount, + operator_receive_address, + ) { + Ok(tx) => Ok(Some(tx)), + Err(e) => bail!("failed to create skip-kickoff txn: {e}"), + } +} + +pub fn operator_sign_kickoff( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, +) -> Result { + let operator_context = graph.parameters.get_operator_context(operator_keypair)?; + let kickoff_connector = graph.parameters.kickoff_connector(); + graph.kickoff.sign_input_0(&operator_context, &kickoff_connector); + Ok(graph.kickoff.tx().clone()) +} + +pub fn operator_sign_take1( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, +) -> Result { + if !graph.committee_pre_signed() { + bail!("missing pre-signatures from committee".to_string()) + }; + let operator_context = graph.parameters.get_operator_context(operator_keypair)?; + let connector_a = graph.connector_a(); + let connector_b = graph.connector_b(); + let guardian_connector = graph.guardian_connector(); + graph.take1.sign_input_1(&operator_context, &connector_a); + graph.take1.sign_input_2(&operator_context, &connector_b); + graph.take1.sign_input_4(&operator_context, &guardian_connector); + Ok(graph.take1.tx().clone()) +} + +pub fn operator_sign_take2( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, +) -> Result { + if !graph.committee_pre_signed() { + bail!("missing pre-signatures from committee".to_string()) + }; + let operator_context = graph.parameters.get_operator_context(operator_keypair)?; + let connector_d = graph.connector_d(); + let connector_f = graph.connector_f(); + let guardian_connector = graph.guardian_connector(); + graph.take2.sign_input_1(&operator_context, &connector_d); + graph.take2.sign_input_2(&operator_context, &connector_f); + graph.take2.sign_input_3(&operator_context, &guardian_connector); + Ok(graph.take2.tx().clone()) +} + +pub fn operator_sign_watchtower_challenge_init( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, +) -> Result { + let operator_context = graph.parameters.get_operator_context(operator_keypair)?; + let connector_b = graph.connector_b(); + graph.watchtower_challenge_init.sign_input_0(&operator_context, &connector_b); + Ok(graph.watchtower_challenge_init.tx().clone()) +} + +pub fn operator_sign_watchtower_challenge_timeout( + operator_keypair: Keypair, + graph: &mut BitvmGcGraph, + watchtower_index: usize, +) -> Result { + if watchtower_index >= graph.watchtower_challenge_timeouts.len() { + bail!("invalid watchtower index {watchtower_index}") + }; + if !graph.committee_pre_signed() { + bail!("missing pre-signatures from committee") + }; + let operator_context = graph.parameters.get_operator_context(operator_keypair)?; + let watchtower_challenge_connector = graph.watchtower_challenge_connector(watchtower_index)?; + graph.watchtower_challenge_timeouts[watchtower_index] + .sign_input_0(&operator_context, &watchtower_challenge_connector); + Ok(graph.watchtower_challenge_timeouts[watchtower_index].tx().clone()) +} + +pub fn operator_sign_challenge_ack( + graph: &BitvmGcGraph, + watchtower_index: usize, + preimage: &[u8], +) -> Result { + if watchtower_index >= graph.parameters.watchtower_pubkeys.len() { + bail!("invalid watchtower index {watchtower_index}") + }; + let ack_connector = graph.ack_connector(watchtower_index)?; + let input = graph + .watchtower_challenge_init + .ack_connector_input(watchtower_index) + .map_err(|e| anyhow::anyhow!("failed to get ack connector input: {e}"))?; + operator_challenge_ack(&ack_connector, preimage, input) + .map_err(|e| anyhow::anyhow!("failed to sign operator challenge ack: {e}")) +} + +pub fn operator_sign_commit_pubin( + graph: &BitvmGcGraph, + wots_secret_key: &OperatorCommitPubinSecretKey, + pubin_commitment: &[u8; 96], +) -> Result { + if Wots96::generate_public_key(wots_secret_key) + != graph.parameters.operator_commit_pubin_wots_pubkey + { + bail!("provided pubin WOTS secret key does not match expected public key") + }; + + let connector_e = graph.connector_e(); + let input = graph + .watchtower_challenge_init + .connector_e_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-e input: {e}"))?; + operator_commit_pubin(&connector_e, pubin_commitment, wots_secret_key, input) + .map_err(|e| anyhow::anyhow!("failed to sign operator commit pubin: {e}")) +} + +pub fn operator_sign_assert( + graph: &mut BitvmGcGraph, + wots_secret_key: &OperatorAssertSecretKey, + proof: &[u8; 96], + pi2: &[u8], + pi3: &[u8], +) -> Result { + if Wots96::generate_public_key(wots_secret_key) != graph.parameters.operator_assert_wots_pubkey + { + bail!("provided WOTS secret key does not match expected public key".to_string()) + }; + + let connector_c = graph.connector_c(); + graph + .operator_assert + .operator_commit_proof(wots_secret_key, &connector_c, proof, pi2, pi3) + .map_err(|e| anyhow::anyhow!("failed to sign operator assert: {e}"))?; + Ok(graph.operator_assert.tx().clone()) +} + +pub fn operator_sign_wrongly_challenged( + graph: &BitvmGcGraph, + verifier_index: usize, + final_msg: &[u8], +) -> Result<(TxIn, Amount)> { + if verifier_index >= graph.verifier_asserts.len() { + bail!("invalid verifier index {verifier_index}".to_string()) + }; + + let prover_connector = graph.prover_connector(verifier_index)?; + let input = graph.verifier_asserts[verifier_index] + .prover_connector_input() + .map_err(|e| anyhow::anyhow!("failed to get prover connector input: {e}"))?; + + let final_msg = final_msg.to_vec(); + wrongly_challenged(&prover_connector, &input, &final_msg) + .map(|txin| (txin, input.amount)) + .map_err(|e| anyhow::anyhow!("failed to sign wrongly challenged: {e}")) +} + +pub fn take1_timelock(network: Network) -> u32 { + take1_timelock_with_config(network, &default_timelock_config(network)) +} + +pub fn take1_timelock_with_config(network: Network, timelock_config: &TimelockConfig) -> u32 { + take1_timelock_blocks(network, timelock_config) +} + +pub fn take2_timelock(network: Network) -> u32 { + take2_timelock_with_config(network, &default_timelock_config(network)) +} + +pub fn take2_timelock_with_config(network: Network, timelock_config: &TimelockConfig) -> u32 { + take2_timelock_blocks(network, timelock_config) +} diff --git a/crates/bitvm2-ga/src/committee/mod.rs b/crates/bitvm-gc/src/operator/mod.rs similarity index 100% rename from crates/bitvm2-ga/src/committee/mod.rs rename to crates/bitvm-gc/src/operator/mod.rs diff --git a/crates/bitvm2-ga/src/pegin.rs b/crates/bitvm-gc/src/pegin.rs similarity index 100% rename from crates/bitvm2-ga/src/pegin.rs rename to crates/bitvm-gc/src/pegin.rs diff --git a/crates/bitvm-gc/src/timelocks.rs b/crates/bitvm-gc/src/timelocks.rs new file mode 100644 index 000000000..72b930ac0 --- /dev/null +++ b/crates/bitvm-gc/src/timelocks.rs @@ -0,0 +1,294 @@ +use anyhow::{Result, bail}; +use bitcoin::Network; +use goat::constants::TimelockConfig; + +pub const NODE_BITCOIN_BLOCK_INTERVAL_SECS: i64 = 600; +pub const NODE_TESTNET_BLOCK_INTERVAL_SECS: i64 = 300; +pub const NODE_SIGNET_BLOCK_INTERVAL_SECS: i64 = 60; +pub const NODE_REGTEST_BLOCK_INTERVAL_SECS: i64 = 60; + +pub const NODE_BITCOIN_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { + connector_z: 144, + connector_a: 144, + prover_connector: 144, + connector_d: 432, + watchtower_challenge: 144, + operator_ack: 288, + operator_commit: 432, + connector_f: 576, +}; +pub const NODE_TESTNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { + connector_z: 100, + connector_a: 6, + prover_connector: 16, + connector_d: 32, + watchtower_challenge: 20, + operator_ack: 28, + operator_commit: 42, + connector_f: 56, +}; +pub const NODE_SIGNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { + connector_z: 6, + connector_a: 6, + prover_connector: 6, + connector_d: 18, + watchtower_challenge: 6, + operator_ack: 12, + operator_commit: 18, + connector_f: 24, +}; +pub const NODE_REGTEST_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { + connector_z: 1, + connector_a: 2, + prover_connector: 1, + connector_d: 3, + watchtower_challenge: 1, + operator_ack: 2, + operator_commit: 3, + connector_f: 4, +}; + +pub fn default_timelock_config(network: Network) -> TimelockConfig { + match network { + Network::Bitcoin => NODE_BITCOIN_TIMELOCK_CONFIG, + Network::Testnet | Network::Testnet4 => NODE_TESTNET_TIMELOCK_CONFIG, + Network::Signet => NODE_SIGNET_TIMELOCK_CONFIG, + Network::Regtest => NODE_REGTEST_TIMELOCK_CONFIG, + } +} + +pub fn estimated_block_interval_secs(network: Network) -> i64 { + match network { + Network::Bitcoin => NODE_BITCOIN_BLOCK_INTERVAL_SECS, + Network::Testnet | Network::Testnet4 => NODE_TESTNET_BLOCK_INTERVAL_SECS, + Network::Signet => NODE_SIGNET_BLOCK_INTERVAL_SECS, + Network::Regtest => NODE_REGTEST_BLOCK_INTERVAL_SECS, + } +} + +/// Non-serialized timing policy used to validate the on-chain timelock config. +/// +/// A graph commits to the CSV values, while every node applies this local network +/// policy before accepting those values. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ProtocolTimingBudget { + /// Confirmation depth required before downstream Bitcoin evidence is used. + pub evidence_confirmations: u32, + /// Event discovery, P2P propagation, scheduling, and local signing. + pub reaction_blocks: u32, + /// P99 time for a watchtower to retrieve and prepare its proof commitment. + pub watchtower_proof_blocks: u32, + /// Target blocks for a transaction to be included, including fee bumping. + pub inclusion_blocks: u32, + /// Reorg and transient service failure allowance. + pub safety_blocks: u32, +} + +impl ProtocolTimingBudget { + const fn action_window(self) -> u32 { + self.reaction_blocks + .saturating_add(self.inclusion_blocks) + .saturating_add(self.safety_blocks) + } + + const fn watchtower_proof_window(self) -> u32 { + self.action_window().saturating_add(self.watchtower_proof_blocks) + } + + const fn confirmed_action_window(self) -> u32 { + self.evidence_confirmations + .saturating_add(self.inclusion_blocks) + .saturating_add(self.safety_blocks) + } +} + +const BITCOIN_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget { + evidence_confirmations: 6, + reaction_blocks: 6, + watchtower_proof_blocks: 24, + inclusion_blocks: 6, + safety_blocks: 6, +}; + +const TESTNET_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget { + evidence_confirmations: 10, + reaction_blocks: 2, + watchtower_proof_blocks: 12, + inclusion_blocks: 2, + safety_blocks: 2, +}; + +const SIGNET_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget { + evidence_confirmations: 1, + reaction_blocks: 1, + watchtower_proof_blocks: 2, + inclusion_blocks: 1, + safety_blocks: 1, +}; + +const REGTEST_TIMING_BUDGET: ProtocolTimingBudget = ProtocolTimingBudget { + evidence_confirmations: 0, + reaction_blocks: 1, + watchtower_proof_blocks: 0, + inclusion_blocks: 0, + safety_blocks: 0, +}; + +pub fn protocol_timing_budget(network: Network) -> ProtocolTimingBudget { + match network { + Network::Bitcoin => BITCOIN_TIMING_BUDGET, + Network::Testnet | Network::Testnet4 => TESTNET_TIMING_BUDGET, + Network::Signet => SIGNET_TIMING_BUDGET, + Network::Regtest => REGTEST_TIMING_BUDGET, + } +} + +pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Result<()> { + let budget = protocol_timing_budget(network); + for (name, value) in [ + ("connector_z", config.connector_z), + ("connector_a", config.connector_a), + ("prover_connector", config.prover_connector), + ("connector_d", config.connector_d), + ("watchtower_challenge", config.watchtower_challenge), + ("operator_ack", config.operator_ack), + ("operator_commit", config.operator_commit), + ("connector_f", config.connector_f), + ] { + if value < budget.reaction_blocks { + bail!( + "timelock_config.{name} must be at least {} reaction blocks, got {value}", + budget.reaction_blocks, + ); + } + } + let default_connector_z = default_timelock_config(network).connector_z; + if config.connector_z != default_connector_z { + bail!( + "timelock_config.connector_z must remain {} because connector-z is fixed before graph construction", + default_connector_z + ); + } + + let action_window = budget.action_window(); + ensure_at_least("connector_a", config.connector_a, action_window)?; + ensure_at_least( + "watchtower_challenge", + config.watchtower_challenge, + budget.watchtower_proof_window(), + )?; + ensure_gap_at_least( + "watchtower_challenge", + config.watchtower_challenge, + "operator_ack", + config.operator_ack, + action_window, + )?; + ensure_gap_at_least( + "max(watchtower_challenge, operator_ack)", + config.watchtower_challenge.max(config.operator_ack), + "operator_commit", + config.operator_commit, + budget.confirmed_action_window(), + )?; + ensure_at_least("prover_connector", config.prover_connector, action_window)?; + ensure_gap_at_least( + "prover_connector", + config.prover_connector, + "connector_d", + config.connector_d, + action_window, + )?; + ensure_gap_at_least( + "operator_commit", + config.operator_commit, + "connector_f", + config.connector_f, + budget.confirmed_action_window(), + )?; + + Ok(()) +} + +fn ensure_gap_at_least( + left_name: &str, + left: u32, + right_name: &str, + right: u32, + required_blocks: u32, +) -> Result<()> { + let actual_blocks = right.saturating_sub(left); + if actual_blocks < required_blocks { + bail!( + "timelock_config.{right_name} - timelock_config.{left_name} must be at least \ + {required_blocks} blocks, got {actual_blocks}" + ); + } + Ok(()) +} + +fn ensure_at_least(name: &str, value: u32, required_blocks: u32) -> Result<()> { + if value < required_blocks { + bail!("timelock_config.{name} must be at least {required_blocks} blocks, got {value}"); + } + Ok(()) +} + +pub fn timelock_blocks(_network: Network, blocks: u32) -> u32 { + blocks +} + +pub fn connector_z_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.connector_z) +} + +pub fn default_connector_z_timelock_blocks(network: Network) -> u32 { + connector_z_timelock_blocks(network, &default_timelock_config(network)) +} + +pub fn take1_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.connector_a) +} + +pub fn take2_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.connector_d) +} + +pub fn connector_f_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.connector_f) +} + +pub fn disprove_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.prover_connector) +} + +pub fn watchtower_challenge_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.watchtower_challenge) +} + +pub fn operator_ack_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.operator_ack) +} + +pub fn operator_commit_timelock_blocks(network: Network, config: &TimelockConfig) -> u32 { + timelock_blocks(network, config.operator_commit) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn all_default_timelock_configs_validate() { + for (network, config) in [ + (Network::Bitcoin, NODE_BITCOIN_TIMELOCK_CONFIG), + (Network::Testnet, NODE_TESTNET_TIMELOCK_CONFIG), + (Network::Testnet4, NODE_TESTNET_TIMELOCK_CONFIG), + (Network::Signet, NODE_SIGNET_TIMELOCK_CONFIG), + (Network::Regtest, NODE_REGTEST_TIMELOCK_CONFIG), + ] { + assert_eq!(default_timelock_config(network), config); + validate_timelock_config(network, &config).unwrap(); + } + } +} diff --git a/crates/bitvm-gc/src/types.rs b/crates/bitvm-gc/src/types.rs new file mode 100644 index 000000000..65e27178f --- /dev/null +++ b/crates/bitvm-gc/src/types.rs @@ -0,0 +1,794 @@ +use std::collections::BTreeMap; + +use anyhow::{Result, bail}; +use bitcoin::{ + Address, Amount, Network, PrivateKey, PublicKey, TxOut, Witness, XOnlyPublicKey, key::Keypair, + taproot::LeafVersion, +}; +use goat::{ + assert_scripts::{ + INPUT_WIRE_NUM, LabelHash, OperatorAssertPublicKey, OperatorCommitPubinPublicKey, WireHash, + }, + connectors::{ + assert_connectors::{ProverConnector, VerifierConnector}, + base::TaprootConnector, + connector_0::Connector0, + connector_a::ConnectorA, + connector_b::ConnectorB, + connector_c::ConnectorC, + connector_d::ConnectorD, + connector_e::ConnectorE, + connector_f::ConnectorF, + connector_z::ConnectorZ, + kickoff_connectors::{ + ForceSkipConnector, GuardianConnector, KickoffConnector, PrekickoffConnector, + }, + watchtower_connectors::{AckConnector, WatchtowerChallengeConnector}, + }, + constants::TimelockConfig, + contexts::{base::BaseContext, committee::CommitteeContext, operator::OperatorContext}, + transactions::{ + assert::{DisproveTransaction, OperatorAssertTransaction, VerifierAssertTransaction}, + base::*, + challenge::ChallengeTransaction, + kickoff::KickoffTransaction, + pegin::*, + pre_signed::PreSignedTransaction, + prekickoff::{ + ChallengeIncompleteKickoffTransaction, ForceSkipKickoffTransaction, + PrekickoffTransaction, QuickChallengeTransaction, + }, + take1::Take1Transaction, + take2::Take2Transaction, + watchtower_challenge::{ + OperatorChallengeNackTransaction, OperatorCommitTimeoutTransaction, + WatchtowerChallengeInitTransaction, WatchtowerChallengeTimeoutTransaction, + }, + }, +}; +use secp256k1::SECP256K1; +use serde::{Deserialize, Serialize}; +use serde_big_array::BigArray; +use sha2::{Digest, Sha256}; +use uuid::Uuid; + +use crate::{ + committee::{CommitteeSignatures, push_committee_pre_signatures}, + operator::{generate_bitvm_graph, push_operator_pre_signature}, + timelocks::validate_timelock_config, +}; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct UserInfo { + pub depositor_evm_address: [u8; 20], + pub txn_fees: [u64; 3], + pub inputs: Vec, + pub user_xonly_pubkey: XOnlyPublicKey, + #[serde(with = "node_serializer::address")] + pub user_change_address: Address, + #[serde(with = "node_serializer::address")] + pub user_refund_address: Address, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct BitvmGcInstanceParameters { + pub network: Network, + pub instance_id: Uuid, + pub user_info: UserInfo, + pub pegin_amount: Amount, + pub committee_pubkeys: Vec, + pub committee_agg_pubkey: PublicKey, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct PrekickoffParameters { + pub cur_prekickoff_txn: PrekickoffTransaction, + pub replenish_fee_inputs: Vec, + pub replenish_fee_prev_outs: Vec, + pub fee_amount: u64, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct BitvmGcGraphParameters { + pub instance_parameters: BitvmGcInstanceParameters, + pub prekickoff_parameters: PrekickoffParameters, + pub timelock_config: TimelockConfig, + pub graph_id: Uuid, + pub graph_nonce: u64, + pub challenge_amount: Amount, + pub operator_pubkey: PublicKey, + #[serde(with = "BigArray")] + pub operator_assert_wots_pubkey: OperatorAssertPublicKey, + #[serde(with = "BigArray")] + pub operator_commit_pubin_wots_pubkey: OperatorCommitPubinPublicKey, + #[serde(with = "node_serializer::address")] + pub operator_receive_address: Address, + pub watchtower_pubkeys: Vec, + pub watchtower_ack_hashlocks: Vec, + pub pubin_disprove_constant: [u8; 32], + pub gc_data: Vec, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct BitvmGcCircuitData { + pub verifier_pubkey: PublicKey, + pub final_msg_hashlocks: Vec, + #[serde(with = "BigArray")] + pub wire_hashes: [WireHash; INPUT_WIRE_NUM], +} + +impl BitvmGcInstanceParameters { + pub fn n_of_n_taproot_public_key(&self) -> XOnlyPublicKey { + XOnlyPublicKey::from(self.committee_agg_pubkey) + } + + pub fn user_taproot_public_key(&self) -> XOnlyPublicKey { + self.user_info.user_xonly_pubkey + } + + pub fn connector_0(&self) -> Connector0 { + Connector0::new(self.network, &self.n_of_n_taproot_public_key()) + } + + pub fn connector_z(&self) -> ConnectorZ { + ConnectorZ::new( + self.network, + &self.n_of_n_taproot_public_key(), + &self.user_taproot_public_key(), + &crate::timelocks::default_timelock_config(self.network), + ) + } + + pub fn build_pegin_tx( + &self, + ) -> Result<(PegInDepositTransaction, PegInConfirmTransaction, PegInRefundTransaction)> { + let network = self.network; + let connector_0 = self.connector_0(); + let connector_z = self.connector_z(); + let pegin_message = [ + get_magic_bytes(&network), + self.instance_id.as_bytes().to_vec(), + self.user_info.depositor_evm_address.to_vec(), + ] + .concat(); + + let pegin_deposit = PegInDepositTransaction::new_unsigned( + &connector_z, + self.user_info.inputs.clone(), + self.pegin_amount + Amount::from_sat(self.user_info.txn_fees[1]), + Amount::from_sat(self.user_info.txn_fees[0]), + self.user_info.user_change_address.clone(), + ) + .map_err(|e| anyhow::anyhow!("fail to build pegin deposit txn: {e}"))?; + let deposit_outpoint = pegin_deposit + .connector_z_input() + .map_err(|e| anyhow::anyhow!("fail to get pegin deposit output: {e}"))?; + let pegin_confirm = PegInConfirmTransaction::new_for_validation( + &connector_0, + &connector_z, + deposit_outpoint.clone(), + Amount::from_sat(self.user_info.txn_fees[1]), + pegin_message, + ) + .map_err(|e| anyhow::anyhow!("fail to build pegin confirm txn: {e}"))?; + let pegin_refund = PegInRefundTransaction::new_for_validation( + &connector_z, + deposit_outpoint, + &self.user_info.user_refund_address, + Amount::from_sat(self.user_info.txn_fees[2]), + ) + .map_err(|e| anyhow::anyhow!("fail to build pegin refund txn: {e}"))?; + + Ok((pegin_deposit, pegin_confirm, pegin_refund)) + } + + pub fn build_pegin_cancel_psbt(&self) -> Result { + let connector_z = self.connector_z(); + let n_of_n_taproot_public_key = self.n_of_n_taproot_public_key(); + + let pegin_deposit = PegInDepositTransaction::new_unsigned( + &connector_z, + self.user_info.inputs.clone(), + self.pegin_amount + Amount::from_sat(self.user_info.txn_fees[1]), + Amount::from_sat(self.user_info.txn_fees[0]), + self.user_info.user_change_address.clone(), + ) + .map_err(|e| anyhow::anyhow!("fail to build pegin deposit txn: {e}"))?; + let deposit_outpoint = pegin_deposit + .connector_z_input() + .map_err(|e| anyhow::anyhow!("fail to get pegin deposit output: {e}"))?; + let pegin_refund = PegInRefundTransaction::new_for_validation( + &connector_z, + deposit_outpoint.clone(), + &self.user_info.user_refund_address, + Amount::from_sat(self.user_info.txn_fees[2]), + ) + .map_err(|e| anyhow::anyhow!("fail to build pegin refund txn: {e}"))?; + + let mut psbt = bitcoin::psbt::Psbt::from_unsigned_tx(pegin_refund.tx().clone()).unwrap(); + let taproot_spend_info = connector_z.generate_taproot_spend_info(); + let mut tap_scripts = BTreeMap::new(); + let tap_script_1 = connector_z.generate_taproot_leaf_script(1); + tap_scripts.insert( + taproot_spend_info + .control_block(&(tap_script_1.clone(), LeafVersion::TapScript)) + .unwrap(), + (tap_script_1, LeafVersion::TapScript), + ); + let psbt_input_0 = bitcoin::psbt::Input { + witness_utxo: { + Some(TxOut { + value: deposit_outpoint.amount, + script_pubkey: connector_z.generate_taproot_address().script_pubkey(), + }) + }, + tap_merkle_root: taproot_spend_info.merkle_root(), + tap_internal_key: Some(n_of_n_taproot_public_key), + tap_scripts, + ..Default::default() + }; + psbt.inputs[0] = psbt_input_0; + + Ok(psbt) + } + + pub fn get_committee_context( + &self, + committee_member_keypair: Keypair, + ) -> Result { + let network = self.network; + let committee_public_key = self.committee_agg_pubkey; + let committee_taproot_public_key = XOnlyPublicKey::from(committee_public_key); + let private_key = PrivateKey::new(committee_member_keypair.secret_key(), network); + let committee_member_public_key = PublicKey::from_private_key(SECP256K1, &private_key); + if !self.committee_pubkeys.contains(&committee_member_public_key) { + bail!("The provided committee member keypair does not match any committee public key"); + } + Ok(CommitteeContext { + network, + committee_keypair: committee_member_keypair, + committee_public_key: committee_member_public_key, + n_of_n_public_keys: self.committee_pubkeys.clone(), + n_of_n_public_key: committee_public_key, + n_of_n_taproot_public_key: committee_taproot_public_key, + }) + } + + pub fn get_base_context(&self) -> BaseBitvmContext { + let network = self.network; + let n_of_n_public_keys = self.committee_pubkeys.clone(); + let n_of_n_public_key = self.committee_agg_pubkey; + let n_of_n_taproot_public_key = XOnlyPublicKey::from(n_of_n_public_key); + BaseBitvmContext { + network, + n_of_n_public_keys, + n_of_n_public_key, + n_of_n_taproot_public_key, + } + } +} + +impl BitvmGcGraphParameters { + pub fn network(&self) -> Network { + self.instance_parameters.network + } + + pub fn operator_taproot_public_key(&self) -> XOnlyPublicKey { + XOnlyPublicKey::from(self.operator_pubkey) + } + + pub fn n_of_n_taproot_public_key(&self) -> XOnlyPublicKey { + self.instance_parameters.n_of_n_taproot_public_key() + } + + pub fn connector_0(&self) -> Connector0 { + self.instance_parameters.connector_0() + } + + pub fn connector_z(&self) -> ConnectorZ { + self.instance_parameters.connector_z() + } + + pub fn prekickoff_connector(&self) -> PrekickoffConnector { + PrekickoffConnector::new(self.network(), &self.operator_taproot_public_key()) + } + + pub fn force_skip_connector(&self) -> ForceSkipConnector { + ForceSkipConnector::new(self.network(), &self.operator_taproot_public_key()) + } + + pub fn kickoff_connector(&self) -> KickoffConnector { + KickoffConnector::new(self.network(), &self.operator_taproot_public_key()) + } + + pub fn connector_a(&self) -> ConnectorA { + ConnectorA::new( + self.network(), + &self.operator_taproot_public_key(), + &self.n_of_n_taproot_public_key(), + &self.timelock_config, + ) + } + + pub fn connector_b(&self) -> ConnectorB { + ConnectorB::new(self.network(), &self.operator_taproot_public_key()) + } + + pub fn connector_c(&self) -> ConnectorC { + ConnectorC::new( + self.network(), + &self.n_of_n_taproot_public_key(), + &self.operator_assert_wots_pubkey, + ) + } + + pub fn connector_d(&self) -> ConnectorD { + ConnectorD::new( + self.network(), + &self.operator_taproot_public_key(), + &self.n_of_n_taproot_public_key(), + &self.operator_commit_pubin_wots_pubkey, + &self.operator_assert_wots_pubkey, + &self.pubin_disprove_constant, + &self.watchtower_ack_hashlocks, + &self.timelock_config, + ) + } + + pub fn connector_e(&self) -> ConnectorE { + ConnectorE::new( + self.network(), + &self.n_of_n_taproot_public_key(), + &self.operator_commit_pubin_wots_pubkey, + &self.timelock_config, + ) + } + + pub fn connector_f(&self) -> ConnectorF { + ConnectorF::new( + self.network(), + &self.operator_taproot_public_key(), + &self.n_of_n_taproot_public_key(), + &self.timelock_config, + ) + } + + pub fn guardian_connector(&self) -> GuardianConnector { + GuardianConnector::new(self.network(), &self.operator_taproot_public_key()) + } + + pub fn watchtower_challenge_connector( + &self, + watchtower_index: usize, + ) -> Result { + let watchtower_taproot_public_key = self + .watchtower_pubkeys + .get(watchtower_index) + .ok_or_else(|| anyhow::anyhow!("invalid watchtower index {watchtower_index}"))?; + Ok(WatchtowerChallengeConnector::new( + self.network(), + &self.operator_taproot_public_key(), + watchtower_taproot_public_key, + &self.timelock_config, + )) + } + + pub fn watchtower_challenge_connectors(&self) -> Vec { + self.watchtower_pubkeys + .iter() + .map(|pubkey| { + WatchtowerChallengeConnector::new( + self.network(), + &self.operator_taproot_public_key(), + pubkey, + &self.timelock_config, + ) + }) + .collect() + } + + pub fn ack_connector(&self, watchtower_index: usize) -> Result { + let hashlock = self + .watchtower_ack_hashlocks + .get(watchtower_index) + .ok_or_else(|| anyhow::anyhow!("invalid watchtower index {watchtower_index}"))?; + Ok(AckConnector::new( + self.network(), + &self.n_of_n_taproot_public_key(), + *hashlock, + &self.timelock_config, + )) + } + + pub fn ack_connectors(&self) -> Vec { + self.watchtower_ack_hashlocks + .iter() + .map(|hashlock| { + AckConnector::new( + self.network(), + &self.n_of_n_taproot_public_key(), + *hashlock, + &self.timelock_config, + ) + }) + .collect() + } + + pub fn verifier_connector(&self, verifier_index: usize) -> Result { + let gc_data = self + .gc_data + .get(verifier_index) + .ok_or_else(|| anyhow::anyhow!("invalid verifier index {verifier_index}"))?; + Ok(VerifierConnector::new( + self.network(), + &self.n_of_n_taproot_public_key(), + &self.operator_assert_wots_pubkey, + gc_data.wire_hashes.clone(), + )) + } + + pub fn verifier_connectors(&self) -> Vec { + self.gc_data + .iter() + .map(|data| { + VerifierConnector::new( + self.network(), + &self.n_of_n_taproot_public_key(), + &self.operator_assert_wots_pubkey, + data.wire_hashes.clone(), + ) + }) + .collect() + } + + pub fn prover_connector(&self, verifier_index: usize) -> Result { + let gc_data = self + .gc_data + .get(verifier_index) + .ok_or_else(|| anyhow::anyhow!("invalid verifier index {verifier_index}"))?; + Ok(ProverConnector::new( + self.network(), + self.n_of_n_taproot_public_key(), + gc_data.final_msg_hashlocks.clone(), + &self.timelock_config, + )) + } + + pub fn prover_connectors(&self) -> Vec { + self.gc_data + .iter() + .map(|data| { + ProverConnector::new( + self.network(), + self.n_of_n_taproot_public_key(), + data.final_msg_hashlocks.clone(), + &self.timelock_config, + ) + }) + .collect() + } + + pub fn validate_timelock_config(&self) -> Result<()> { + validate_timelock_config(self.network(), &self.timelock_config) + } + + pub fn get_operator_context(&self, operator_keypair: Keypair) -> Result { + let network = self.instance_parameters.network; + let operator_public_key = self.operator_pubkey; + let operator_taproot_public_key = XOnlyPublicKey::from(operator_public_key); + let committee_public_key = self.instance_parameters.committee_agg_pubkey; + let committee_taproot_public_key = XOnlyPublicKey::from(committee_public_key); + if operator_public_key + != PublicKey::from_private_key( + SECP256K1, + &PrivateKey::new(operator_keypair.secret_key(), network), + ) + { + bail!("The provided operator keypair does not match the operator public key"); + } + Ok(OperatorContext { + network, + operator_keypair, + operator_public_key, + operator_taproot_public_key, + + n_of_n_public_keys: self.instance_parameters.committee_pubkeys.clone(), + n_of_n_public_key: committee_public_key, + n_of_n_taproot_public_key: committee_taproot_public_key, + }) + } + + pub fn get_base_context(&self) -> BaseBitvmContext { + self.instance_parameters.get_base_context() + } +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct BitvmGcGraph { + pub(crate) operator_pre_signed: bool, + pub(crate) committee_pre_signed: bool, + pub parameters: BitvmGcGraphParameters, + + pub cur_prekickoff: PrekickoffTransaction, + pub next_prekickoff: PrekickoffTransaction, + pub force_skip_kickoff: ForceSkipKickoffTransaction, + pub quick_challenge: QuickChallengeTransaction, + pub challenge_incomplete_kickoff: ChallengeIncompleteKickoffTransaction, + + pub pegin: PegInConfirmTransaction, + pub kickoff: KickoffTransaction, + pub take1: Take1Transaction, + pub challenge: ChallengeTransaction, + pub watchtower_challenge_init: WatchtowerChallengeInitTransaction, + pub watchtower_challenge_timeouts: Vec, + pub operator_challenge_nacks: Vec, + pub operator_commit_timeout: OperatorCommitTimeoutTransaction, + pub operator_assert: OperatorAssertTransaction, + pub verifier_asserts: Vec, + pub disproves: Vec, + pub take2: Take2Transaction, +} + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] +pub struct SimplifiedBitvmGcGraph { + pub(crate) operator_pre_signed: bool, + pub(crate) committee_pre_signed: bool, + pub parameters: BitvmGcGraphParameters, + pub operator_pre_sigs: Option>, + pub committee_pre_sigs: Option, +} + +impl BitvmGcGraph { + pub fn operator_pre_signed(&self) -> bool { + self.operator_pre_signed + } + pub fn committee_pre_signed(&self) -> bool { + self.committee_pre_signed + } + pub fn connector_0(&self) -> Connector0 { + self.parameters.connector_0() + } + pub fn connector_z(&self) -> ConnectorZ { + self.parameters.connector_z() + } + pub fn prekickoff_connector(&self) -> PrekickoffConnector { + self.parameters.prekickoff_connector() + } + pub fn force_skip_connector(&self) -> ForceSkipConnector { + self.parameters.force_skip_connector() + } + pub fn kickoff_connector(&self) -> KickoffConnector { + self.parameters.kickoff_connector() + } + pub fn connector_a(&self) -> ConnectorA { + self.parameters.connector_a() + } + pub fn connector_b(&self) -> ConnectorB { + self.parameters.connector_b() + } + pub fn connector_c(&self) -> ConnectorC { + self.parameters.connector_c() + } + pub fn connector_d(&self) -> ConnectorD { + self.parameters.connector_d() + } + pub fn connector_e(&self) -> ConnectorE { + self.parameters.connector_e() + } + pub fn connector_f(&self) -> ConnectorF { + self.parameters.connector_f() + } + pub fn guardian_connector(&self) -> GuardianConnector { + self.parameters.guardian_connector() + } + pub fn ack_connector(&self, watchtower_index: usize) -> Result { + self.parameters.ack_connector(watchtower_index) + } + pub fn ack_connectors(&self) -> Vec { + self.parameters.ack_connectors() + } + pub fn watchtower_challenge_connector( + &self, + watchtower_index: usize, + ) -> Result { + self.parameters.watchtower_challenge_connector(watchtower_index) + } + pub fn watchtower_challenge_connectors(&self) -> Vec { + self.parameters.watchtower_challenge_connectors() + } + pub fn verifier_connector(&self, verifier_index: usize) -> Result { + self.parameters.verifier_connector(verifier_index) + } + pub fn verifier_connectors(&self) -> Vec { + self.parameters.verifier_connectors() + } + pub fn prover_connector(&self, verifier_index: usize) -> Result { + self.parameters.prover_connector(verifier_index) + } + pub fn prover_connectors(&self) -> Vec { + self.parameters.prover_connectors() + } + pub fn to_simplified(&self) -> Result { + fn extract_sig_from_witness(witness: &Witness) -> Result { + witness + .nth(0) + .and_then(|data| bitcoin::taproot::Signature::from_slice(data).ok()) + .ok_or_else(|| anyhow::anyhow!("No valid signature found in witness")) + } + let operator_pre_sigs = if self.operator_pre_signed { + Some(vec![ + self.force_skip_kickoff.tx().input[0].witness.clone(), + self.force_skip_kickoff.tx().input[1].witness.clone(), + self.quick_challenge.tx().input[0].witness.clone(), + self.quick_challenge.tx().input[1].witness.clone(), + self.challenge_incomplete_kickoff.tx().input[0].witness.clone(), + self.challenge_incomplete_kickoff.tx().input[1].witness.clone(), + ]) + } else { + None + }; + let committee_pre_sigs = if self.committee_pre_signed { + let take1 = vec![ + extract_sig_from_witness(&self.take1.tx().input[0].witness)?, + extract_sig_from_witness(&self.take1.tx().input[3].witness)?, + ]; + let take2 = vec![extract_sig_from_witness(&self.take2.tx().input[0].witness)?]; + let challenge = vec![extract_sig_from_witness(&self.challenge.tx().input[0].witness)?]; + let mut watchtower_challenge_timeout = Vec::new(); + for tx in &self.watchtower_challenge_timeouts { + watchtower_challenge_timeout + .push(extract_sig_from_witness(&tx.tx().input[1].witness)?); + } + let mut operator_challenge_nack = Vec::new(); + for tx in &self.operator_challenge_nacks { + operator_challenge_nack.push(extract_sig_from_witness(&tx.tx().input[0].witness)?); + operator_challenge_nack.push(extract_sig_from_witness(&tx.tx().input[1].witness)?); + } + let operator_commit_timeout = vec![ + extract_sig_from_witness(&self.operator_commit_timeout.tx().input[0].witness)?, + extract_sig_from_witness(&self.operator_commit_timeout.tx().input[1].witness)?, + ]; + let mut disprove = Vec::new(); + for disprove_tx in &self.disproves { + disprove.push(extract_sig_from_witness(&disprove_tx.tx().input[0].witness)?); + disprove.push(extract_sig_from_witness(&disprove_tx.tx().input[1].witness)?); + } + Some(CommitteeSignatures { + take1, + take2, + challenge, + watchtower_challenge_timeout, + operator_challenge_nack, + operator_commit_timeout, + disprove, + }) + } else { + None + }; + Ok(SimplifiedBitvmGcGraph { + operator_pre_signed: self.operator_pre_signed, + committee_pre_signed: self.committee_pre_signed, + parameters: self.parameters.clone(), + operator_pre_sigs, + committee_pre_sigs, + }) + } + pub fn from_simplified(simplified: &SimplifiedBitvmGcGraph) -> Result { + let mut graph = generate_bitvm_graph(simplified.parameters.clone())?; + if simplified.operator_pre_signed { + let operator_pre_sigs = simplified + .operator_pre_sigs + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing operator pre signatures"))?; + push_operator_pre_signature(&mut graph, operator_pre_sigs)?; + graph.operator_pre_signed = true; + } + if simplified.committee_pre_signed { + let committee_pre_sigs = simplified + .committee_pre_sigs + .as_ref() + .ok_or_else(|| anyhow::anyhow!("Missing committee pre signatures"))?; + push_committee_pre_signatures(&mut graph, committee_pre_sigs)?; + graph.committee_pre_signed = true; + } + Ok(graph) + } +} + +impl SimplifiedBitvmGcGraph { + pub fn operator_pre_signed(&self) -> bool { + self.operator_pre_signed + } + + pub fn committee_pre_signed(&self) -> bool { + self.committee_pre_signed + } + + pub fn parameters_hash(&self) -> Result<[u8; 32]> { + self.parameters.canonical_graph_params_hash() + } +} + +impl BitvmGcInstanceParameters { + pub fn parameters_hash(&self) -> Result<[u8; 32]> { + let encoded = serde_json::to_vec(self)?; + Ok(Sha256::digest(encoded).into()) + } +} + +impl BitvmGcGraphParameters { + pub fn canonical_graph_params_hash(&self) -> Result<[u8; 32]> { + let encoded = serde_json::to_vec(self)?; + let mut hasher = Sha256::new(); + hasher.update(b"GOAT_BITVM_GC_GRAPH_PARAMS_V1"); + hasher.update((encoded.len() as u64).to_be_bytes()); + hasher.update(encoded); + Ok(hasher.finalize().into()) + } + + pub fn parameters_hash(&self) -> Result<[u8; 32]> { + self.canonical_graph_params_hash() + } +} + +impl SimplifiedBitvmGcGraph { + pub fn canonical_graph_params_hash(&self) -> Result<[u8; 32]> { + self.parameters.canonical_graph_params_hash() + } +} + +pub struct BaseBitvmContext { + pub network: Network, + pub n_of_n_public_keys: Vec, + pub n_of_n_public_key: PublicKey, + pub n_of_n_taproot_public_key: XOnlyPublicKey, +} + +impl BaseContext for BaseBitvmContext { + fn network(&self) -> Network { + self.network + } + fn n_of_n_public_keys(&self) -> &Vec { + &self.n_of_n_public_keys + } + fn n_of_n_public_key(&self) -> &PublicKey { + &self.n_of_n_public_key + } + fn n_of_n_taproot_public_key(&self) -> &XOnlyPublicKey { + &self.n_of_n_taproot_public_key + } +} + +pub fn get_magic_bytes(net: &Network) -> Vec { + match net { + Network::Bitcoin => hex::encode(b"GTV6").as_bytes().to_vec(), + _ => hex::encode(b"GTT6").as_bytes().to_vec(), + } +} + +pub mod node_serializer { + use bitcoin::Address; + use serde::{Deserialize, Deserializer, Serializer}; + use std::str::FromStr; + + pub mod address { + use super::*; + + pub fn serialize(addr: &Address, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&addr.to_string()) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + Address::from_str(&s) + .map(|addr| addr.assume_checked()) + .map_err(serde::de::Error::custom) + } + } +} diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs new file mode 100644 index 000000000..92526b5f6 --- /dev/null +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -0,0 +1,464 @@ +use crate::{ + babe_adapter::TxAssertWitness, + timelocks::{default_timelock_config, disprove_timelock_blocks}, + types::BitvmGcGraph, +}; +use anyhow::{Result, bail}; +use ark_bn254::{G1Affine, G2Affine}; +use ark_serialize::CanonicalSerialize; +use bitcoin::{ + Address, Amount, Network, ScriptBuf, Transaction, TxIn, TxOut, script::read_scriptint, +}; +use bitvm::chunk::api::type_conversion_utils::RawWitness; +use goat::{ + assert_scripts::{INPUT_WIRE_NUM, Label, PROVER_SIG_LEN}, + connectors::{base::TaprootConnector, connector_d::CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX}, + constants::TimelockConfig, + scripts::{generate_opreturn_script, p2a_output}, + transactions::{ + assert::{pubin_disprove, validate_pubin}, + base::{DUST_AMOUNT, output_topology}, + pre_signed::PreSignedTransaction, + watchtower_challenge::extract_operator_preimage_from_ack_txin, + }, + wots::{WOTS96_BASE, Wots, Wots96}, +}; + +/// challenge has a pre-signed SinglePlusAnyoneCanPay input and output +/// get incomplete tx here, add inputs with enough amount, then broadcast it to start challnege progress +pub fn export_challenge_tx(graph: &BitvmGcGraph) -> Result<(Transaction, Amount)> { + if !graph.committee_pre_signed() { + bail!("missing pre-signatures from committee") + }; + Ok((graph.challenge.tx().clone(), graph.challenge.challenge_amount)) +} + +/// return true if anchor output is added +/// return false if change output is added or no output is added +fn add_change_or_anchor_output( + tx: &mut Transaction, + total_input_amount: Amount, + change_address: Address, + fee_rate: f64, +) -> Result { + let dust_amount = Amount::from_sat(DUST_AMOUNT); + let output_amount = tx.output.iter().map(|o| o.value).sum(); + tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: change_address.script_pubkey() }); + let min_relay_fee = 1.0; + let min_fee_amount = + Amount::from_sat((tx.weight().to_vbytes_ceil() as f64 * min_relay_fee).ceil() as u64); + let fee_amount = + Amount::from_sat((tx.weight().to_vbytes_ceil() as f64 * fee_rate).ceil() as u64); + if min_fee_amount + dust_amount + output_amount > total_input_amount { + bail!("insufficient input amount to cover min relay fee"); + } + if fee_amount + output_amount + dust_amount < total_input_amount { + // add change output + let change_amount = total_input_amount - fee_amount - output_amount; + tx.output.last_mut().unwrap().value = change_amount; + Ok(false) + } else if fee_amount + output_amount > total_input_amount { + // add anchor output + tx.output.pop(); + tx.output.push(p2a_output()); + Ok(true) + } else { + // not add any output since remaining is just enough to cover fee + tx.output.pop(); + Ok(false) + } +} + +/// return (tx, true) if anchor output is added, subsequently challenger need to cover fee via CPFP +/// return (tx, false) if change output is added or no output is added, challenger can directly broadcast it +pub fn build_force_skip_kickoff_tx( + graph: &BitvmGcGraph, + verifier_receive_address: Address, + fee_rate: f64, +) -> Result<(Transaction, bool)> { + if !graph.operator_pre_signed() { + bail!("missing pre-signatures from operator") + }; + let mut tx = graph.force_skip_kickoff.tx().clone(); + let total_input_amount = graph.force_skip_kickoff.prev_outs().iter().map(|o| o.value).sum(); + let anchor_added = add_change_or_anchor_output( + &mut tx, + total_input_amount, + verifier_receive_address, + fee_rate, + )?; + Ok((tx, anchor_added)) +} + +/// return (tx, true) if anchor output is added, subsequently challenger need to cover fee via CPFP +/// return (tx, false) if change output is added or no output is added, challenger can directly broadcast it +pub fn build_quick_challenge_tx( + graph: &BitvmGcGraph, + verifier_receive_address: Address, + fee_rate: f64, +) -> Result<(Transaction, bool)> { + if !graph.operator_pre_signed() { + bail!("missing pre-signatures from operator") + }; + let mut tx = graph.quick_challenge.tx().clone(); + let total_input_amount = graph.quick_challenge.prev_outs().iter().map(|o| o.value).sum(); + let anchor_added = add_change_or_anchor_output( + &mut tx, + total_input_amount, + verifier_receive_address, + fee_rate, + )?; + Ok((tx, anchor_added)) +} + +/// return (tx, true) if anchor output is added, subsequently challenger need to cover fee via CPFP +/// return (tx, false) if change output is added or no output is added, challenger can directly broadcast it +pub fn build_challenge_incomplete_kickoff_tx( + graph: &BitvmGcGraph, + verifier_receive_address: Address, + fee_rate: f64, +) -> Result<(Transaction, bool)> { + if !graph.operator_pre_signed() { + bail!("missing pre-signatures from operator") + }; + let mut tx = graph.challenge_incomplete_kickoff.tx().clone(); + let total_input_amount = + graph.challenge_incomplete_kickoff.prev_outs().iter().map(|o| o.value).sum(); + let anchor_added = add_change_or_anchor_output( + &mut tx, + total_input_amount, + verifier_receive_address, + fee_rate, + )?; + Ok((tx, anchor_added)) +} + +fn split_operator_assert_wots_and_extra_data( + mut operator_assertion: RawWitness, +) -> Result<(RawWitness, Vec, Vec)> { + let expected_len = PROVER_SIG_LEN + 2; + if operator_assertion.len() != expected_len { + bail!( + "operator assert witness has {} stack items; expected {expected_len}", + operator_assertion.len() + ); + } + let pi3 = operator_assertion + .pop() + .ok_or_else(|| anyhow::anyhow!("operator assert witness is empty"))?; + let pi2 = operator_assertion + .pop() + .ok_or_else(|| anyhow::anyhow!("operator assert witness is missing pi2"))?; + Ok((operator_assertion, pi2, pi3)) +} + +fn operator_assert_wots_signature(operator_assertion: &RawWitness) -> Result> { + if operator_assertion.len() != PROVER_SIG_LEN { + bail!( + "operator assert WOTS witness has {} stack items; expected {PROVER_SIG_LEN}", + operator_assertion.len() + ); + } + + let mut wots_sig = Vec::with_capacity(Wots96::TOTAL_DIGIT_LEN as usize); + for i in (0..operator_assertion.len()).step_by(2) { + let digit_signature = &operator_assertion[i]; + let digit_value_bytes = &operator_assertion[i + 1]; + if digit_signature.len() != 20 { + bail!( + "operator assert WOTS digit signature has {} bytes; expected 20", + digit_signature.len() + ); + } + if digit_value_bytes.len() > 2 { + bail!( + "operator assert WOTS digit value has {} bytes; expected at most 2", + digit_value_bytes.len() + ); + } + let digit_value = read_scriptint(digit_value_bytes) + .map_err(|e| anyhow::anyhow!("invalid operator assert WOTS digit value: {e}"))?; + if !(0..WOTS96_BASE as i64).contains(&digit_value) { + bail!("operator assert WOTS digit value {digit_value} is out of range"); + } + + let mut item = [0u8; 21]; + item[..20].copy_from_slice(digit_signature); + item[20] = digit_value as u8; + wots_sig.push(item); + } + Ok(wots_sig) +} + +fn validate_operator_assert_extra_data(pi2: &[u8], pi3: &[u8]) -> Result<()> { + let pi2_len = G2Affine::default().compressed_size(); + let pi3_len = G1Affine::default().compressed_size(); + if pi2.len() != pi2_len { + bail!("operator assert pi2 has {} bytes; expected {pi2_len}", pi2.len()); + } + if pi3.len() != pi3_len { + bail!("operator assert pi3 has {} bytes; expected {pi3_len}", pi3.len()); + } + Ok(()) +} + +pub fn extract_operator_assert_witness( + graph: &BitvmGcGraph, + operator_assert_txin: &TxIn, +) -> Result { + let operator_assertion = + graph + .connector_c() + .extract_leaf_1_raw_witness(operator_assert_txin) + .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; + let (operator_assertion, pi2, pi3) = + split_operator_assert_wots_and_extra_data(operator_assertion)?; + + let wots_sig = operator_assert_wots_signature(&operator_assertion)?; + validate_operator_assert_extra_data(&pi2, &pi3)?; + let assert_witness = TxAssertWitness { wots_sig, pi2, pi3 }; + if assert_witness.recover_pi2_pi3().is_none() { + bail!("operator assert extra-data cannot recover pi2 and pi3"); + } + Ok(assert_witness) +} + +pub fn extract_operator_assert_witness_for_challenge( + graph: &BitvmGcGraph, + operator_assert_txin: &TxIn, +) -> Result { + let operator_assertion = + graph + .connector_c() + .extract_leaf_1_raw_witness(operator_assert_txin) + .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; + let (operator_assertion, pi2, pi3) = + split_operator_assert_wots_and_extra_data(operator_assertion)?; + let wots_sig = operator_assert_wots_signature(&operator_assertion)?; + let (pi2, pi3) = if validate_operator_assert_extra_data(&pi2, &pi3).is_ok() { + (pi2, pi3) + } else { + (Vec::new(), Vec::new()) + }; + Ok(TxAssertWitness { wots_sig, pi2, pi3 }) +} + +pub fn build_verifier_assert_tx( + graph: &BitvmGcGraph, + operator_assert_txin: TxIn, + verifier_index: usize, + labels: [Label; INPUT_WIRE_NUM], +) -> Result { + if verifier_index >= graph.verifier_asserts.len() { + bail!("invalid verifier index {verifier_index}") + }; + + let connector_c = graph.connector_c(); + let operator_assertion = connector_c + .extract_leaf_1_raw_witness(&operator_assert_txin) + .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; + let (operator_assertion, _pi2, _pi3) = + split_operator_assert_wots_and_extra_data(operator_assertion)?; + + let verifier_connector = graph.verifier_connector(verifier_index)?; + let mut verifier_assert = graph.verifier_asserts[verifier_index].clone(); + verifier_assert + .verifier_publish_labels(&verifier_connector, labels, &operator_assertion) + .map_err(|e| anyhow::anyhow!("failed to build verifier assert: {e}"))?; + Ok(verifier_assert.tx().clone()) +} + +pub fn build_disprove_tx( + graph: &BitvmGcGraph, + verifier_index: usize, + verifier_receive_address: Option<[u8; 20]>, +) -> Result { + if verifier_index >= graph.disproves.len() { + bail!("invalid verifier index {verifier_index}") + }; + if !graph.committee_pre_signed { + bail!("missing pre-signatures from committee") + }; + let mut disprove_tx = graph.disproves[verifier_index].tx().clone(); + if let Some(verifier_receive_address) = verifier_receive_address { + disprove_tx.output.insert( + 0, + TxOut { + value: Amount::ZERO, + script_pubkey: generate_opreturn_script(verifier_receive_address.to_vec()), + }, + ); + } + Ok(disprove_tx) +} + +pub fn validate_pubin_disprove( + graph: &BitvmGcGraph, + operator_commit_pubin_txin: &TxIn, + operator_assert_txin: &TxIn, + operator_ack_txins: &[TxIn], +) -> Result> { + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + let mut ack_preimages = vec![vec![]; watchtower_num]; + for txin in operator_ack_txins { + let vout = txin.previous_output.vout as usize; + if vout % 2 != 1 { + bail!("invalid ack txin in operator_ack_txins, unexpected vout: {vout}"); + } + let watchtower_index = vout / 2; + if watchtower_index >= watchtower_num + || vout != output_topology::watchtower_challenge_init::ack_connector(watchtower_index) + { + bail!("invalid ack txin in operator_ack_txins, unexpected vout: {vout}"); + } + let preimage = extract_operator_preimage_from_ack_txin(txin) + .map_err(|e| anyhow::anyhow!("failed to extract preimage from ack txin: {e}"))?; + ack_preimages[watchtower_index] = preimage; + } + + let connector_e = graph.connector_e(); + let connector_c = graph.connector_c(); + let operator_commit_pubin_witness = connector_e + .extract_leaf_0_raw_witness(operator_commit_pubin_txin) + .map_err(|e| anyhow::anyhow!("failed to extract operator commit pubin witness: {e}"))?; + let operator_assert_witness = connector_c + .extract_leaf_1_raw_witness(operator_assert_txin) + .map_err(|e| anyhow::anyhow!("failed to extract operator assert witness: {e}"))?; + let (operator_assert_witness, _pi2, _pi3) = + split_operator_assert_wots_and_extra_data(operator_assert_witness)?; + let connector_d = graph.connector_d(); + let input_lock_script = + connector_d.generate_taproot_leaf_script(CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX); + + Ok(validate_pubin( + operator_commit_pubin_witness, + operator_assert_witness, + ack_preimages, + input_lock_script, + )) +} + +pub fn build_pubin_disprove_txin( + graph: &BitvmGcGraph, + input_script_witness: RawWitness, +) -> Result { + let connector_d = graph.connector_d(); + let connector_d_input = graph + .operator_assert + .connector_d_input() + .map_err(|e| anyhow::anyhow!("failed to get connector-d input: {e}"))?; + pubin_disprove(&connector_d, &connector_d_input, input_script_witness) + .map_err(|e| anyhow::anyhow!("failed to build pubin-disprove txin: {e}")) +} + +pub fn disprove_timelock(network: Network) -> u32 { + disprove_timelock_with_config(network, &default_timelock_config(network)) +} + +pub fn disprove_timelock_with_config(network: Network, timelock_config: &TimelockConfig) -> u32 { + disprove_timelock_blocks(network, timelock_config) +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin::{ + OutPoint, TxOut, XOnlyPublicKey, + secp256k1::{Keypair, SECP256K1, SecretKey}, + }; + use goat::{ + connectors::{ + base::TaprootConnector, + connector_d::{CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX, ConnectorD}, + }, + scripts::{generate_opreturn_script, p2a_output}, + transactions::base::{Input, pubin_disprove_input_amount}, + }; + + #[test] + fn pubin_disprove_with_witness_is_standard_sized() { + let operator_secret = SecretKey::from_slice(&[1; 32]).expect("valid operator secret"); + let committee_secret = SecretKey::from_slice(&[2; 32]).expect("valid committee secret"); + let operator_keypair = Keypair::from_secret_key(SECP256K1, &operator_secret); + let committee_keypair = Keypair::from_secret_key(SECP256K1, &committee_secret); + let operator_key = + XOnlyPublicKey::from(bitcoin::PublicKey::from(operator_keypair.public_key())); + let committee_key = + XOnlyPublicKey::from(bitcoin::PublicKey::from(committee_keypair.public_key())); + + let guest_secret = Wots96::generate_secret_key(); + let guest_public_key = Wots96::generate_public_key(&guest_secret); + let assert_secret = Wots96::generate_secret_key(); + let assert_public_key = Wots96::generate_public_key(&assert_secret); + let expected_constant = [0x22; 32]; + let mut guest_pubin = [0u8; 96]; + guest_pubin[..32].fill(0x11); + guest_pubin[32..64].fill(0x23); + guest_pubin[64..].fill(0xff); + let operator_assert_pubin = [0u8; 96]; + + let connector_d = ConnectorD::new( + Network::Testnet4, + &operator_key, + &committee_key, + &guest_public_key, + &assert_public_key, + &expected_constant, + &vec![], + &default_timelock_config(Network::Testnet4), + ); + let assert_witness = + Wots96::sign_to_raw_witness(&assert_secret, &operator_assert_pubin).to_vec(); + let guest_witness = Wots96::sign_to_raw_witness(&guest_secret, &guest_pubin).to_vec(); + assert!( + validate_pubin( + guest_witness.clone(), + assert_witness.clone(), + vec![], + connector_d.generate_taproot_leaf_script(CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX), + ) + .is_some(), + "locally constructed pubin-disprove witness must satisfy Connector-D", + ); + let mut witness = assert_witness; + witness.extend(guest_witness); + let input = pubin_disprove( + &connector_d, + &Input { + outpoint: OutPoint::null(), + amount: Amount::from_sat(pubin_disprove_input_amount()), + }, + witness, + ) + .expect("build pubin-disprove input"); + let tx = Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![input], + output: vec![ + p2a_output(), + TxOut { + value: Amount::ZERO, + script_pubkey: generate_opreturn_script(b"pubin-disprove".to_vec()), + }, + ], + }; + + let base_size = tx.base_size(); + let witness_size = tx.input[0].witness.size(); + let total_size = tx.total_size(); + println!( + "pubin-disprove tx sizes: base_size={base_size}, witness_size={witness_size}, total_size={total_size}" + ); + assert_eq!( + total_size, + base_size + witness_size + 2, + "pubin-disprove total size must include the SegWit marker/flag and script witness: total={total_size}, base={base_size}, witness={witness_size}", + ); + assert!( + tx.weight() <= Transaction::MAX_STANDARD_WEIGHT, + "pubin-disprove exceeds Bitcoin Core's standard transaction limit: weight={}, total_size={total_size}", + tx.weight().to_wu(), + ); + } +} diff --git a/crates/bitvm2-ga/src/operator/mod.rs b/crates/bitvm-gc/src/verifier/mod.rs similarity index 100% rename from crates/bitvm2-ga/src/operator/mod.rs rename to crates/bitvm-gc/src/verifier/mod.rs diff --git a/crates/bitvm-gc/src/watchtower/api.rs b/crates/bitvm-gc/src/watchtower/api.rs new file mode 100644 index 000000000..9ce8d1a44 --- /dev/null +++ b/crates/bitvm-gc/src/watchtower/api.rs @@ -0,0 +1,39 @@ +use anyhow::{Result, bail}; +use bitcoin::{Address, Amount, Transaction, key::Keypair}; +use goat::transactions::{base::Input, watchtower_challenge::watchtower_challenge}; + +use crate::types::BitvmGcGraph; + +pub fn estimate_watchtower_challenge_vbytes(commitment_data_len: usize) -> usize { + 120 + commitment_data_len.saturating_mul(12) / 10 +} + +pub fn build_watchtower_challenge_tx( + graph: &BitvmGcGraph, + watchtower_keypair: &Keypair, + watchtower_index: usize, + commitment_data: &[u8], + payer_inputs: Vec, + change_address: &Address, + fee_amount: Amount, +) -> Result { + if watchtower_index >= graph.parameters.watchtower_pubkeys.len() { + bail!("Invalid watchtower index"); + } + let watchtower_challenge_connector = graph.watchtower_challenge_connector(watchtower_index)?; + let input_0 = graph + .watchtower_challenge_init + .watchtower_connector_input(watchtower_index) + .map_err(|e| anyhow::anyhow!("failed to get watchtower connector input: {e}"))?; + + watchtower_challenge( + watchtower_keypair, + &watchtower_challenge_connector, + commitment_data, + input_0, + payer_inputs, + change_address, + fee_amount, + ) + .map_err(|e| anyhow::anyhow!("failed to build watchtower challenge transaction: {e}")) +} diff --git a/crates/bitvm2-ga/src/watchtower/mod.rs b/crates/bitvm-gc/src/watchtower/mod.rs similarity index 100% rename from crates/bitvm2-ga/src/watchtower/mod.rs rename to crates/bitvm-gc/src/watchtower/mod.rs diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs new file mode 100644 index 000000000..eed8d2500 --- /dev/null +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -0,0 +1,373 @@ +use ark_bn254::Fr; +use ark_crypto_primitives::snark::CircuitSpecificSetupSNARK; +use ark_ec::AffineRepr; +use ark_groth16::Groth16; +use bitvm_gc::assert_scripts::{INPUT_WIRE_NUM, label_hash}; +use bitvm_gc::babe_adapter::{ + BABE_M_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, BabeVerifierState, + CACSetupPackage, ChallengeAssertWitnessRaw, FinalizedInstanceData, SolderingData, + WOTS_SIG_COUNT, build_assert_witness, build_challenge_assert_witness, build_real_setup_package, + build_setup_package, build_wrongly_challenged_witness, + build_wrongly_challenged_witness_from_preimages, derive_finalized_indices, + extract_gc_circuit_data, open_and_solder, open_real_setup_and_solder, + sample_cac_instance_commit, sample_finalized_instance_data, verify_real_setup, verify_setup, +}; +use rand::SeedableRng; +use rand_chacha::ChaCha12Rng; +use std::collections::HashSet; +use std::str::FromStr; +use verifiable_circuit_babe::babe::DummyMulCircuit; + +fn verifier_pubkey() -> bitcoin::PublicKey { + bitcoin::PublicKey::from_str( + "0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798", + ) + .expect("public key") +} + +#[test] +#[ignore = "requires FGC_GATES_PATH, FGC_OUT_INDICES_PATH, SGC_GATES_PATH, SGC_OUT_INDICES_PATH, FGC_COMPACT_GATES_PATH, FGC_COMPACT_OUT_INDICES_PATH, SGC_COMPACT_GATES_PATH, SGC_COMPACT_OUT_INDICES_PATH runtime assets"] +fn real_setup_restores_private_state_and_verifies_soldering_proof() { + let mut rng = ChaCha12Rng::seed_from_u64(42); + let a = Fr::from(3_u64); + let b = Fr::from(7_u64); + let (_, vk) = Groth16::::setup( + DummyMulCircuit:: { a: Some(a), b: Some(b) }, + &mut rng, + ) + .expect("groth16 setup"); + let static_public_inputs = a * b; + let _dynamic_public_inputs = a * a; + + let (package, private_state) = + build_real_setup_package(BABE_M_CC, &vk, static_public_inputs).expect("real setup"); + let restored = + serde_json::from_slice(&serde_json::to_vec(&private_state).expect("serialize state")) + .expect("deserialize state"); + let soldering_builder = BabeBundleBuilder::new(); + let finalized_indices = (0..BABE_M_CC).collect::>(); + let (opened, finalized, soldering) = open_real_setup_and_solder( + &soldering_builder, + &restored, + &package, + &finalized_indices, + &vk, + static_public_inputs, + ) + .expect("open real setup"); + + assert!(opened.is_empty()); + assert_eq!(finalized.len(), BABE_M_CC); + verify_real_setup( + &soldering_builder, + &package, + &opened, + &finalized, + &soldering, + &vk, + &finalized_indices, + static_public_inputs, + ) + .expect("verify soldering proof"); + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) + .expect("extract native 768-wire graph data"); +} + +#[test] +fn babe_setup_payload_round_trips_and_derives_gc_data() { + let package = CACSetupPackage { + commits: (0..BABE_M_CC).map(|index| sample_cac_instance_commit(index as u8)).collect(), + }; + + let encoded = serde_json::to_vec(&package).expect("serialize package"); + let decoded: CACSetupPackage = serde_json::from_slice(&encoded).expect("deserialize package"); + assert_eq!(decoded, package); + + let finalized_indices = + derive_finalized_indices(&decoded, BABE_M_CC).expect("derive finalized"); + assert_eq!(finalized_indices.len(), BABE_M_CC); + + let finalized = finalized_indices + .iter() + .map(|index| sample_finalized_instance_data(*index)) + .collect::>(); + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let gc_data = + extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs).expect("extract gc data"); + + assert_eq!(gc_data.verifier_pubkey, verifier_pubkey()); + assert_eq!(gc_data.final_msg_hashlocks.len(), BABE_M_CC); +} + +#[test] +fn protocol_finalized_instances_contribute_one_base_wire_slot() { + use verifiable_circuit_babe::babe::GC_INPUT_WIRES; + let package = CACSetupPackage { + commits: (0..BABE_M_CC).map(|i| sample_cac_instance_commit(i as u8)).collect(), + }; + let finalized: Vec = + (0..BABE_M_CC).map(sample_finalized_instance_data).collect(); + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let epk = &package.commits[finalized[0].index].epk; + + let gc_data = + extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs).expect("one verifier graph slot"); + + // M finalized instances each contribute one hashlock … + assert_eq!(gc_data.final_msg_hashlocks, h_msgs); + let n = GC_INPUT_WIRES / 3; // N_PADDED = 256 wires per group, no dummy positions + // EPK maps 1:1 to wire_hashes: pi1_x[0..n], pi1_y[n..2n], x_d[2n..3n] + assert_eq!(gc_data.wire_hashes[0].false_label_hash, epk[0][0]); + assert_eq!(gc_data.wire_hashes[0].true_label_hash, epk[0][1]); + assert_eq!(gc_data.wire_hashes[n - 1].false_label_hash, epk[n - 1][0]); + assert_eq!(gc_data.wire_hashes[n].false_label_hash, epk[n][0]); + assert_eq!(gc_data.wire_hashes[2 * n].false_label_hash, epk[2 * n][0]); + assert_eq!(gc_data.wire_hashes[3 * n - 1].false_label_hash, epk[3 * n - 1][0]); +} + +#[test] +fn gc_slot_rejects_invalid_epk_length() { + use verifiable_circuit_babe::babe::GC_INPUT_WIRES; + let h_msgs = vec![[0u8; 20]; BABE_M_CC]; + for bad_len in [0, 1, GC_INPUT_WIRES - 1, GC_INPUT_WIRES + 1] { + let bad_epk = vec![[[0u8; 20]; 2]; bad_len]; + match extract_gc_circuit_data(verifier_pubkey(), &bad_epk, &h_msgs) { + Ok(_) => panic!("epk length {bad_len} should be rejected"), + Err(err) => assert!( + err.to_string().contains("epk has"), + "unexpected error for len {bad_len}: {err}" + ), + } + } +} + +#[test] +fn finalized_indices_reject_invalid_counts_and_cover_full_cut() { + let package = build_setup_package(4).expect("setup package"); + + assert!(derive_finalized_indices(&package, 0).is_err()); + assert!(derive_finalized_indices(&package, 5).is_err()); + + let first = derive_finalized_indices(&package, 4).expect("derive full cut"); + assert_eq!(first.len(), 4); + assert_eq!(first.iter().copied().collect::>().len(), 4); + assert_eq!(first.iter().copied().collect::>(), HashSet::from([0, 1, 2, 3])); +} + +#[test] +fn verify_setup_accepts_valid_opening_and_rejects_invalid_shapes() { + let package = build_setup_package(4).expect("setup package"); + let finalized_indices = derive_finalized_indices(&package, 2).expect("derive finalized"); + let (opened, finalized, soldering) = + open_and_solder(&package, &finalized_indices).expect("open and solder"); + + verify_setup(&package, &opened, &finalized, &soldering).expect("valid setup"); + + let mut duplicate_finalized = finalized.clone(); + duplicate_finalized.push(finalized[0].clone()); + assert!(verify_setup(&package, &opened, &duplicate_finalized, &soldering).is_err()); + + let mut overlapping_opened = opened.clone(); + overlapping_opened.push((finalized[0].index, 0)); + assert!(verify_setup(&package, &overlapping_opened, &finalized, &soldering).is_err()); + + let mut wrong_seed_opened = opened.clone(); + wrong_seed_opened[0].1 ^= 1; + assert!(verify_setup(&package, &wrong_seed_opened, &finalized, &soldering).is_err()); + + let mut mismatched_soldering = soldering.clone(); + mismatched_soldering.finalized_indices.reverse(); + if mismatched_soldering.finalized_indices == soldering.finalized_indices { + mismatched_soldering.finalized_indices.push(usize::MAX); + } + assert!(verify_setup(&package, &opened, &finalized, &mismatched_soldering).is_err()); +} + +#[test] +fn witness_builders_validate_inputs_and_indices() { + use bitvm_gc::operator::generate_assert_wots_key; + let (assert_secret_key, _) = generate_assert_wots_key("test-graph-scoped-key"); + let proof = ark_groth16::Proof:: { + a: ark_bn254::G1Affine { + x: ark_bn254::Fq::from(1u64), + y: ark_bn254::Fq::from(2u64), + infinity: false, + }, + b: ark_bn254::G2Affine::generator(), + c: ark_bn254::G1Affine::generator(), + }; + let dynamic_input = Fr::from(0u64); + + let assert_witness = + build_assert_witness(&proof, &assert_secret_key, dynamic_input).expect("assert witness"); + assert_eq!(assert_witness.wots_sig.len(), WOTS_SIG_COUNT); + assert!(assert_witness.try_recover_pi1_xd().is_some()); + assert!(build_assert_witness(&proof, &Vec::new(), dynamic_input).is_err()); + + let verifier_state = BabeVerifierState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized_indices: (0..BABE_M_CC).collect(), + verifier_pubkey: verifier_pubkey(), + }; + let challenge_witness = build_challenge_assert_witness(&verifier_state, &assert_witness, 12) + .expect("challenge witness"); + assert_eq!(challenge_witness.verifier_index, 12); + assert_eq!(challenge_witness.witness.input_labels.len(), INPUT_WIRE_NUM); + + let final_msg = b"finalized-preimage-0".to_vec(); + let h_msgs: Vec<[u8; 20]> = (0..BABE_M_CC) + .map(|i| label_hash(&format!("finalized-preimage-{i}").into_bytes())) + .collect(); + let prover_state = BabeProverState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized: (0..BABE_M_CC).map(sample_finalized_instance_data).collect(), + soldering: SolderingData::sample((0..BABE_M_CC).collect()), + h_msgs, + }; + let wrongly_challenged = + build_wrongly_challenged_witness(&prover_state, &challenge_witness, final_msg.clone()) + .expect("wrongly challenged witness"); + assert_eq!(wrongly_challenged.verifier_index, 12); + assert_eq!(wrongly_challenged.final_msg, final_msg); + assert!( + build_wrongly_challenged_witness( + &prover_state, + &challenge_witness, + b"missing-preimage".to_vec(), + ) + .is_err() + ); +} + +#[test] +fn wrongly_challenged_witness_accepts_valid_preimage_and_rejects_invalid() { + let h_msgs: Vec<[u8; 20]> = (0..BABE_M_CC) + .map(|i| label_hash(&format!("finalized-preimage-{i}").into_bytes())) + .collect(); + let valid_msg = b"finalized-preimage-0".to_vec(); + let challenge_witness = BabeChallengeAssertWitness { + verifier_index: 0, + witness: ChallengeAssertWitnessRaw { input_labels: vec![], wots_sig: vec![] }, + }; + + let from_preimages = build_wrongly_challenged_witness_from_preimages( + &h_msgs, + &challenge_witness, + valid_msg.clone(), + ) + .expect("wrongly challenged witness"); + assert_eq!(from_preimages.verifier_index, 0); + assert_eq!(from_preimages.final_msg, valid_msg); + + let prover_state = BabeProverState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized: (0..BABE_M_CC).map(sample_finalized_instance_data).collect(), + soldering: SolderingData::sample((0..BABE_M_CC).collect()), + h_msgs, + }; + let delegated = build_wrongly_challenged_witness( + &prover_state, + &challenge_witness, + from_preimages.final_msg.clone(), + ) + .expect("delegated wrongly challenged witness"); + assert_eq!(delegated, from_preimages); + + assert!( + build_wrongly_challenged_witness_from_preimages( + &prover_state.h_msgs, + &challenge_witness, + b"not-a-valid-preimage".to_vec(), + ) + .is_err() + ); +} + +#[test] +fn setup_package_and_open_reject_degenerate_inputs() { + assert!(build_setup_package(0).is_err()); + + let pkg = build_setup_package(4).expect("setup package"); + assert!(open_and_solder(&pkg, &[0, 0]).is_err()); // duplicate finalized index + assert!(open_and_solder(&pkg, &[99]).is_err()); // out-of-range finalized index +} + +#[test] +fn witness_builders_reject_wrong_finalized_count() { + use bitvm_gc::babe_adapter::TxAssertWitness; + + // build_challenge_assert_witness rejects finalized_indices.len() != BABE_M_CC + let bad_state = BabeVerifierState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized_indices: vec![0], // 1, not BABE_M_CC + verifier_pubkey: verifier_pubkey(), + }; + let dummy_assert = TxAssertWitness { wots_sig: vec![], pi2: vec![], pi3: vec![] }; + assert!(build_challenge_assert_witness(&bad_state, &dummy_assert, 0).is_err()); + + // build_wrongly_challenged_witness_from_preimages rejects h_msgs.len() != BABE_M_CC + let challenge = BabeChallengeAssertWitness { + verifier_index: 0, + witness: ChallengeAssertWitnessRaw { input_labels: vec![], wots_sig: vec![] }, + }; + assert!( + build_wrongly_challenged_witness_from_preimages(&[], &challenge, b"msg".to_vec(),).is_err() + ); + let too_many = vec![[0u8; 20]; BABE_M_CC + 1]; + assert!( + build_wrongly_challenged_witness_from_preimages(&too_many, &challenge, b"msg".to_vec(),) + .is_err() + ); +} + +#[test] +fn assert_witness_preserves_pi1_and_dynamic_input() { + use bitvm_gc::babe_adapter::assert_wots_message; + use bitvm_gc::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("round-trip-test"); + let dynamic_input = Fr::from(42u64); + let proof = ark_groth16::Proof:: { + a: ark_bn254::G1Affine { + x: ark_bn254::Fq::from(1u64), + y: ark_bn254::Fq::from(2u64), + infinity: false, + }, + b: ark_bn254::G2Affine::generator(), + c: ark_bn254::G1Affine::generator(), + }; + + let assert_witness = build_assert_witness(&proof, &sk, dynamic_input).expect("assert witness"); + + let (recovered_pi1, recovered_xd) = + assert_witness.try_recover_pi1_xd().expect("recover pi1 and xd"); + assert_eq!(recovered_pi1, proof.a); + assert_eq!(recovered_xd, dynamic_input); + + // assert_wots_message must be deterministic + let msg = assert_wots_message(&assert_witness).expect("wots message"); + let msg2 = assert_wots_message(&assert_witness).expect("wots message 2"); + assert_eq!(msg, msg2); +} + +#[test] +fn assert_wots_message_works_for_invalid_field_elements() { + use bitvm::signatures::Wots; + use bitvm_gc::babe_adapter::{TxAssertWitness, assert_wots_message}; + use goat::wots::Wots96; + + // Construct a WOTS sig over bytes that are NOT valid Fq/Fr field elements (all 0xFF). + let sk = Wots96::generate_secret_key(); + let raw_msg = [0xFFu8; 96]; + let sig = Wots96::sign(&sk, &raw_msg); + let witness = TxAssertWitness { wots_sig: sig.to_vec(), pi2: vec![], pi3: vec![] }; + + // try_recover_pi1_xd fails because 0xFF..FF > field modulus + assert!(witness.try_recover_pi1_xd().is_none()); + // assert_wots_message succeeds regardless — it works on raw bytes only + let msg = assert_wots_message(&witness).expect("raw message extraction must succeed"); + assert_eq!(msg, raw_msg); +} diff --git a/crates/bitvm-gc/tests/regtest.rs b/crates/bitvm-gc/tests/regtest.rs new file mode 100644 index 000000000..57b804155 --- /dev/null +++ b/crates/bitvm-gc/tests/regtest.rs @@ -0,0 +1,1242 @@ +use anyhow::{Context, Result, anyhow, bail, ensure}; +use ark_bn254::{Bn254, Fr, G1Affine, G2Affine}; +use ark_ec::AffineRepr; +use ark_groth16::Proof; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use bitcoin::hashes::{Hash, hash160}; +use bitcoin::key::Keypair; +use bitcoin::secp256k1::{SECP256K1, SecretKey}; +use bitcoin::{ + Address, Amount, EcdsaSighashType, Network, OutPoint, PublicKey, ScriptBuf, Sequence, + Transaction, TxIn, TxOut, Txid, XOnlyPublicKey, absolute, transaction, +}; +use bitcoin_script::script; +use bitvm_gc::babe_adapter::{ + BABE_M_CC, BabeProverState, CACSetupPackage, TxAssertWitness, assert_wots_message, + build_assert_witness, build_setup_package, derive_finalized_indices, extract_gc_circuit_data, + open_and_solder, recover_operator_proof_from_assert_witness, verify_setup, +}; +use bitvm_gc::committee::{ + agg_and_push_pegin_confirm_sigs, committee_pre_sign, generate_nonce_from_seed, key_aggregation, + nonce_aggregation, nonces_aggregation, push_committee_pre_signatures, sign_pegin_confirm, + signature_aggregation, verify_graph_committee_pre_signatures, verify_nonce_signatures, +}; +use bitvm_gc::keys::{CommitteeMasterKey, OperatorMasterKey}; +use bitvm_gc::operator::{ + generate_bitvm_graph, operator_pre_sign, operator_sign_assert, operator_sign_challenge_ack, + operator_sign_commit_pubin, operator_sign_kickoff, operator_sign_prekickoff_input_0, + operator_sign_take1, operator_sign_take2, operator_sign_watchtower_challenge_init, + operator_sign_watchtower_challenge_timeout, operator_sign_wrongly_challenged, + verify_graph_operator_pre_signatures, +}; +use bitvm_gc::timelocks::{ + connector_f_timelock_blocks, default_timelock_config, disprove_timelock_blocks, + operator_ack_timelock_blocks, operator_commit_timelock_blocks, take1_timelock_blocks, + take2_timelock_blocks, watchtower_challenge_timelock_blocks, +}; +use bitvm_gc::types::{ + BitvmGcCircuitData, BitvmGcGraph, BitvmGcGraphParameters, BitvmGcInstanceParameters, + PrekickoffParameters, UserInfo, +}; +use bitvm_gc::verifier::{ + build_disprove_tx, build_pubin_disprove_txin, build_verifier_assert_tx, export_challenge_tx, + validate_pubin_disprove, +}; +use bitvm_gc::watchtower::{build_watchtower_challenge_tx, estimate_watchtower_challenge_vbytes}; +use esplora_client::AsyncClient as EsploraClient; +use goat::assert_scripts::{INPUT_WIRE_NUM, Label, WireHash, label_hash}; +use goat::connectors::base::TaprootConnector; +use goat::connectors::kickoff_connectors::{ + ForceSkipConnector, KickoffConnector, PrekickoffConnector, +}; +use goat::scripts::{generate_opreturn_script, p2a_output}; +use goat::transactions::base::{DUST_AMOUNT, Input}; +use goat::transactions::pre_signed::PreSignedTransaction; +use goat::transactions::prekickoff::PrekickoffTransaction; +use goat::transactions::signing::populate_p2wsh_witness; +use reqwest::Client; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; +use serde_json::{Value, json}; +use sha2::{Digest, Sha256}; +use std::fs; +use std::path::{Path, PathBuf}; +use std::sync::OnceLock; +use std::time::{Duration, Instant}; +use tokio::time::sleep; +use uuid::Uuid; + +const REGTEST_ESPLORA_URL: &str = "http://127.0.0.1:3002"; +const REGTEST_RPC_URL: &str = "http://127.0.0.1:18443/wallet/alice"; +const FIXTURE_VERSION: u32 = 1; +const FIXTURE_FILE: &str = "bitvm-gc-regtest-proof-gc-v1.bin"; +const DEFAULT_FEE_SATS: u64 = 1_000; +const PREKICKOFF_AMOUNT_SATS: u64 = 500_000; +const PEGIN_AMOUNT_SATS: u64 = 100_000_000; +const PAYER_AMOUNT_SATS: u64 = 5_000_000; +const FEE_RATE_SAT_PER_VBYTE: u64 = 2; +const CONFIRM_TIMEOUT: Duration = Duration::from_secs(60); + +static MOCK_FIXTURE: OnceLock> = OnceLock::new(); + +#[derive(Serialize, Deserialize)] +struct MockProofGcFixture { + version: u32, + setup_package: CACSetupPackage, + opened: Vec<(usize, u64)>, + prover_state: BabeProverState, + gc_data: BitvmGcCircuitData, + proof_bytes: Vec, +} + +struct TestKeys { + user: Keypair, + operator: Keypair, + challenger: Keypair, + committee: Vec, + verifier: Keypair, + watchtowers: Vec, +} + +struct RegtestRpc { + client: Client, + url: String, + user: String, + password: String, +} + +struct RegtestGraph { + graph: BitvmGcGraph, + keys: TestKeys, + assert_witness: TxAssertWitness, + challenge_labels: Vec