From 06d048f2604b48d57a5acc193e8054e96fe13b1e Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Thu, 18 Jun 2026 21:59:49 +0800 Subject: [PATCH 01/14] BitvmGC V2 (#403) * add bitvm gc msg * gc version init commit * gc update * add verfier contract support & set version to 0.4 * support multi hashlocks for wrongly challenged * Refactor: remove the attestation of the part_stark_vk * feat: add operator wrapper proof * Refactor: remove x_d * feat: commit operator_vk_hash on chain * remove unused constant * feat: update sequencer set hash query to use 'at or before' logic * feat: add operator_vk_hash to sequencer commands and update README * feat: add backward compatibility for operator and commit chain circuits * implement operator wrapper proof functionality * add fixed watchtower public key verification * add bitvm gc msg * gc version init commit * gc update * fix todo * use multi hashlocks * fix rebase * adapt to babe and sig * fix rebase * fix fmt and clippy * fix build * update rpc service * adapt babe state path to configurable env * Only the verifier and the operator need to initialize soldering_builder. * print operator wrapper vk hash during task creation * support bincode serialization for GOATMessage, add legacy JSON deserialization fallback * update BABE_M_CC to 7 * use `par_iter` to parallelize instance creation in babe_adapter * add support for soldering proof chunking, compact payloads, and parallelized setup expansion * implement soldering proof payload range requests * fix missing feature * add soldering upload functionality and deprecate range-based pull logic * allow external socket address override via env * remove soldering proof upload functionality, add s3 and local shared storage * update BitVM and Goat dependencies to new repository and branch * improve error handling in GOATMessage serialization, add JSON fallback on bincode failure * Only the GenCircuits message is encoded using `bincode`, and adds some logs * add BitcoinSPV support for posting single and batch block hashes, fix typo in error message * use the given goat_block_number instead of the one query from the db when init_genesis is true * gc-v2 * replace hardcoded input index with topology helpers * remove unnecessary check * gc-v2 init commit * gc-v2 * gc-v2 * gc v2 --------- Co-authored-by: Blake Co-authored-by: Blake <0xblake.sg@gmail.com> Co-authored-by: ethan --- .github/workflows/ci.yml | 9 +- .gitignore | 4 +- Cargo.lock | 1164 ++- Cargo.toml | 27 +- circuits/README.md | 10 +- circuits/commit-chain-proof/guest/Cargo.lock | 12 +- circuits/commit-chain-proof/guest/Cargo.toml | 2 - circuits/commit-chain-proof/host/src/lib.rs | 11 +- circuits/cron-header-chain-proof.sh | 31 +- circuits/cron-state-chain-proof.sh | 31 +- .../output3.bin.proof_part_stark_vk.bin | Bin 44 -> 0 bytes circuits/header-chain-proof/guest/Cargo.lock | 12 +- circuits/header-chain-proof/guest/Cargo.toml | 2 - circuits/operator-proof/guest/Cargo.lock | 25 +- circuits/operator-proof/guest/Cargo.toml | 6 +- circuits/operator-proof/guest/build.rs | 68 + circuits/operator-proof/guest/src/main.rs | 57 +- circuits/operator-proof/host/Cargo.toml | 2 +- circuits/operator-proof/host/build.rs | 9 + circuits/operator-proof/host/src/lib.rs | 183 +- .../operator-wrapper-proof/guest/Cargo.lock | 6696 +++++++++++++++++ .../operator-wrapper-proof/guest/Cargo.toml | 33 + .../operator-wrapper-proof/guest/src/main.rs | 39 + .../operator-wrapper-proof/host/Cargo.toml | 25 + circuits/operator-wrapper-proof/host/build.rs | 5 + .../operator-wrapper-proof/host/src/lib.rs | 302 + .../operator-wrapper-proof/host/src/main.rs | 24 + circuits/proof-builder/Cargo.toml | 6 +- circuits/proof-builder/src/lib.rs | 71 +- circuits/run-operator-wrapper-proof.sh | 51 + circuits/state-chain-proof/guest/Cargo.lock | 16 +- circuits/state-chain-proof/guest/Cargo.toml | 2 - circuits/watchtower-proof/guest/Cargo.lock | 24 +- circuits/watchtower-proof/guest/Cargo.toml | 2 - circuits/watchtower-proof/guest/src/main.rs | 15 +- circuits/watchtower-proof/host/src/lib.rs | 115 +- .../bitcoin-light-client-circuit/Cargo.toml | 8 +- .../src/attestation.rs | 1494 ---- .../bitcoin-light-client-circuit/src/lib.rs | 915 +-- .../bitcoin-light-client-circuit/src/utils.rs | 5 +- crates/{bitvm2-ga => bitvm-gc}/Cargo.toml | 24 +- crates/{bitvm2-ga => bitvm-gc}/src/actors.rs | 2 +- crates/bitvm-gc/src/babe_adapter.rs | 1083 +++ crates/bitvm-gc/src/committee/api.rs | 615 ++ .../src/committee}/mod.rs | 0 crates/{bitvm2-ga => bitvm-gc}/src/keys.rs | 111 +- crates/{bitvm2-ga => bitvm-gc}/src/lib.rs | 5 +- crates/bitvm-gc/src/operator/api.rs | 597 ++ .../src/operator}/mod.rs | 0 crates/{bitvm2-ga => bitvm-gc}/src/pegin.rs | 0 crates/bitvm-gc/src/types.rs | 725 ++ crates/bitvm-gc/src/verifier/api.rs | 219 + .../operator => bitvm-gc/src/verifier}/mod.rs | 0 crates/bitvm-gc/src/watchtower/api.rs | 39 + .../src/watchtower/mod.rs | 0 crates/bitvm-gc/tests/babe_adapter.rs | 269 + crates/bitvm2-ga/src/challenger/api.rs | 278 - crates/bitvm2-ga/src/committee/api.rs | 682 -- crates/bitvm2-ga/src/operator/api.rs | 980 --- crates/bitvm2-ga/src/tests.rs | 1497 ---- crates/bitvm2-ga/src/types.rs | 747 -- crates/bitvm2-ga/src/watchtower/api.rs | 74 - crates/client/src/goat_chain/chain_adaptor.rs | 24 +- crates/client/src/goat_chain/evmchain.rs | 18 +- crates/client/src/goat_chain/goat_adaptor.rs | 159 +- .../src/goat_chain/mock_goat_adaptor.rs | 15 +- crates/client/src/goat_chain/mod.rs | 26 +- crates/client/src/goat_chain/utils.rs | 2 +- crates/commit-chain/Cargo.toml | 10 +- crates/commit-chain/src/commit_chain.rs | 330 +- crates/commit-chain/src/lib.rs | 21 +- crates/header-chain/Cargo.toml | 2 +- crates/header-chain/src/header_chain.rs | 1 - crates/header-chain/src/lib.rs | 13 +- crates/state-chain/Cargo.toml | 10 +- crates/state-chain/src/lib.rs | 14 +- crates/state-chain/src/state_chain.rs | 1 - ...13379bf8df51054958887f27081ff7f8f47cc.json | 12 - ...d0095ecb645fdfd91d202ce6aba0220349737.json | 12 + .../20250814113519_create_graph_table.sql | 9 +- ...60504120000_create_wrapper_proof_table.sql | 30 + ...000000_create_pending_graph_init_table.sql | 9 + .../20260611000000_add_gc_v2_graph_txids.sql | 8 + crates/store/src/localdb.rs | 555 +- crates/store/src/schema.rs | 68 +- crates/verifier/Cargo.toml | 10 + crates/verifier/src/lib.rs | 23 + .../proof-builder-rpc/proof-builder.toml | 8 +- docs/part-stark-vk-attest-cli.md | 281 - node/Cargo.toml | 23 +- node/README.md | 98 +- node/entrypoint.sh | 2 +- node/src/action.rs | 161 +- node/src/bin/db_inject.rs | 33 +- .../src/bin/fetch_watchtower_xonly_pubkeys.rs | 108 + node/src/bin/mock_rpc.rs | 373 + node/src/bin/part_stark_vk_attest.rs | 246 - node/src/bin/send_bridge_out.rs | 2 +- node/src/bin/send_challenge.rs | 6 +- node/src/bin/send_pegin_request.rs | 16 +- node/src/bin/send_pegout.rs | 6 +- node/src/bin/send_rbf.rs | 6 +- node/src/bin/sequencer-set-publish.rs | 57 +- node/src/env.rs | 151 +- node/src/handle.rs | 2707 ++++--- node/src/lib.rs | 198 +- node/src/main.rs | 29 +- node/src/middleware/behaviour.rs | 1 - node/src/middleware/swarm.rs | 28 +- node/src/p2p_msg_handler.rs | 17 +- node/src/rpc_service/auth.rs | 6 +- node/src/rpc_service/{bitvm2.rs => bitvm.rs} | 35 +- .../src/rpc_service/handler/bitvm2_handler.rs | 467 +- node/src/rpc_service/handler/node_handler.rs | 12 +- node/src/rpc_service/handler/proof_handler.rs | 6 +- node/src/rpc_service/mod.rs | 86 +- node/src/rpc_service/node.rs | 1 + node/src/rpc_service/routes.rs | 3 + node/src/rpc_service/validation.rs | 8 +- node/src/scheduled_tasks/event_watch_task.rs | 30 +- .../graph_maintenance_tasks.rs | 1981 ++--- .../instance_maintenance_tasks.rs | 8 +- node/src/scheduled_tasks/mod.rs | 23 +- .../scheduled_tasks/node_maintenance_tasks.rs | 2 +- .../sequencer_set_hash_monitor_task.rs | 10 +- node/src/soldering_payload_store.rs | 175 + node/src/utils.rs | 2447 +++--- node/src/vk.rs | 5 +- node/ssp-ci.sh | 20 +- proof-builder-rpc/Cargo.toml | 2 + proof-builder-rpc/README.md | 2 +- proof-builder-rpc/proof-builder.toml.example | 6 + proof-builder-rpc/src/api/mod.rs | 8 +- proof-builder-rpc/src/api/proof_handler.rs | 147 +- proof-builder-rpc/src/api/routes.rs | 2 + proof-builder-rpc/src/config.rs | 2 + proof-builder-rpc/src/task/mod.rs | 114 +- proof-builder-rpc/src/task/operator_proof.rs | 22 +- proof-builder-rpc/src/task/wrapper_proof.rs | 259 + 139 files changed, 19139 insertions(+), 12149 deletions(-) delete mode 100644 circuits/data/watchtower/output3.bin.proof_part_stark_vk.bin create mode 100644 circuits/operator-proof/guest/build.rs create mode 100644 circuits/operator-wrapper-proof/guest/Cargo.lock create mode 100644 circuits/operator-wrapper-proof/guest/Cargo.toml create mode 100644 circuits/operator-wrapper-proof/guest/src/main.rs create mode 100644 circuits/operator-wrapper-proof/host/Cargo.toml create mode 100644 circuits/operator-wrapper-proof/host/build.rs create mode 100644 circuits/operator-wrapper-proof/host/src/lib.rs create mode 100644 circuits/operator-wrapper-proof/host/src/main.rs create mode 100755 circuits/run-operator-wrapper-proof.sh rename crates/{bitvm2-ga => bitvm-gc}/Cargo.toml (72%) rename crates/{bitvm2-ga => bitvm-gc}/src/actors.rs (93%) create mode 100644 crates/bitvm-gc/src/babe_adapter.rs create mode 100644 crates/bitvm-gc/src/committee/api.rs rename crates/{bitvm2-ga/src/challenger => bitvm-gc/src/committee}/mod.rs (100%) rename crates/{bitvm2-ga => bitvm-gc}/src/keys.rs (89%) rename crates/{bitvm2-ga => bitvm-gc}/src/lib.rs (77%) create mode 100644 crates/bitvm-gc/src/operator/api.rs rename crates/{bitvm2-ga/src/committee => bitvm-gc/src/operator}/mod.rs (100%) rename crates/{bitvm2-ga => bitvm-gc}/src/pegin.rs (100%) create mode 100644 crates/bitvm-gc/src/types.rs create mode 100644 crates/bitvm-gc/src/verifier/api.rs rename crates/{bitvm2-ga/src/operator => bitvm-gc/src/verifier}/mod.rs (100%) create mode 100644 crates/bitvm-gc/src/watchtower/api.rs rename crates/{bitvm2-ga => bitvm-gc}/src/watchtower/mod.rs (100%) create mode 100644 crates/bitvm-gc/tests/babe_adapter.rs delete mode 100644 crates/bitvm2-ga/src/challenger/api.rs delete mode 100644 crates/bitvm2-ga/src/committee/api.rs delete mode 100644 crates/bitvm2-ga/src/operator/api.rs delete mode 100644 crates/bitvm2-ga/src/tests.rs delete mode 100644 crates/bitvm2-ga/src/types.rs delete mode 100644 crates/bitvm2-ga/src/watchtower/api.rs delete mode 100644 crates/store/.sqlx/query-a574aa126ca49083135ef445cb213379bf8df51054958887f27081ff7f8f47cc.json create mode 100644 crates/store/.sqlx/query-cc6525abb198055f787dd0bfcc0d0095ecb645fdfd91d202ce6aba0220349737.json create mode 100644 crates/store/migrations/20260504120000_create_wrapper_proof_table.sql create mode 100644 crates/store/migrations/20260524000000_create_pending_graph_init_table.sql create mode 100644 crates/store/migrations/20260611000000_add_gc_v2_graph_txids.sql create mode 100644 crates/verifier/Cargo.toml create mode 100644 crates/verifier/src/lib.rs delete mode 100644 docs/part-stark-vk-attest-cli.md create mode 100644 node/src/bin/fetch_watchtower_xonly_pubkeys.rs create mode 100644 node/src/bin/mock_rpc.rs delete mode 100644 node/src/bin/part_stark_vk_attest.rs rename node/src/rpc_service/{bitvm2.rs => bitvm.rs} (97%) create mode 100644 node/src/soldering_payload_store.rs create mode 100644 proof-builder-rpc/src/task/wrapper_proof.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 554b2c10..28495239 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,13 @@ 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 runs-on: ubuntu-latest @@ -67,7 +70,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 cargo test -r --all --all-targets diff --git a/.gitignore b/.gitignore index 13c8f4ab..6d9263a0 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,6 @@ circuits/*/*/*.bin.in **/*.out **/*/output.data* -proof-builder-rpc/*.ckpt \ No newline at end of file +proof-builder-rpc/*.ckpt + +runner \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 8f8f2862..2f6a4613 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", ] @@ -1325,7 +1325,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1336,7 +1336,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1345,6 +1345,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 +2004,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 +2069,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 +2685,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 +2707,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 +2729,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" @@ -2295,7 +2759,7 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.10.5", "log", "prettyplease 0.2.37", "proc-macro2", @@ -2360,20 +2824,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 +2832,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 +2859,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 +2876,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 +2891,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 +2901,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 +2911,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 +2928,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 +2939,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 +2984,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 +3003,75 @@ dependencies = [ ] [[package]] -name = "bitvm2-lib" -version = "0.3.3" +name = "bitvm" +version = "0.1.0" +source = "git+https://github.com/KSlashh/BitVM.git?branch=gc-v2#94193a6fb5d09c1b2419254c7dfcb2bd7b5bc0d2" +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-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/KSlashh/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", "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,14 +3079,16 @@ 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/KSlashh/BitVM.git?branch=gc-v2)", + "bitvm-gc", "borsh", "cbft-rpc", "clap", @@ -2663,12 +3109,13 @@ dependencies = [ "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", @@ -2686,7 +3133,9 @@ dependencies = [ "util", "uuid 1.23.0", "zeroize", + "zkm-recursion-core", "zkm-sdk", + "zkm-stark", "zkm-verifier", ] @@ -2742,6 +3191,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" @@ -2884,6 +3342,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 +3421,7 @@ dependencies = [ [[package]] name = "cbft-rpc" -version = "0.3.3" +version = "0.4.0" dependencies = [ "anyhow", "async-trait", @@ -3065,7 +3533,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 +3591,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 +3619,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 +3634,7 @@ dependencies = [ "bs58", "coins-core", "digest 0.10.7", - "hmac", + "hmac 0.12.1", "k256", "serde", "sha2 0.10.9", @@ -3175,7 +3649,7 @@ checksum = "3db8fba409ce3dc04f7d804074039eb68b960b0829161f8e06c95fea3f122528" dependencies = [ "bitvec", "coins-bip32", - "hmac", + "hmac 0.12.1", "once_cell", "pbkdf2 0.12.2", "rand 0.8.5", @@ -3195,7 +3669,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 +3705,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 +3724,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", @@ -3330,6 +3804,12 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" 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" version = "0.4.3" @@ -3457,6 +3937,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 +4059,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 +4109,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 +4353,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 +4474,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" @@ -4304,7 +4824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4312,8 +4832,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 +4863,7 @@ dependencies = [ "ctr", "digest 0.10.7", "hex", - "hmac", + "hmac 0.12.1", "pbkdf2 0.11.0", "rand 0.8.5", "scrypt", @@ -4632,7 +5152,7 @@ dependencies = [ "ethers-core", "glob", "home", - "md-5", + "md-5 0.10.6", "num_cpus", "once_cell", "path-slash", @@ -5053,6 +5573,33 @@ dependencies = [ "byteorder", ] +[[package]] +name = "garbled-snark-verifier" +version = "0.1.0" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +dependencies = [ + "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", + "poseidon2", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rayon", + "serde", + "serde_json", + "serial_test", +] + [[package]] name = "gcd" version = "2.3.0" @@ -5190,7 +5737,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/KSlashh/BitVM.git?branch=gc-v2#94193a6fb5d09c1b2419254c7dfcb2bd7b5bc0d2" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -5202,10 +5749,10 @@ 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/KSlashh/BitVM.git?branch=gc-v2)", "clap", "colored", "hex", @@ -5217,6 +5764,7 @@ dependencies = [ "reqwest 0.11.27", "secp256k1 0.29.1", "serde", + "serde-big-array", "serde_json", "serial_test", "sha2 0.10.9", @@ -5471,28 +6019,28 @@ dependencies = [ [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ - "bitcoin 0.32.8", + "bitcoin", "borsh", "crypto-bigint", "hex-literal", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", "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", @@ -5541,12 +6089,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 +6214,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 +6226,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" @@ -5812,6 +6363,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 +6427,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 +6443,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 +6991,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 +7812,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" @@ -7479,7 +8039,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", @@ -7622,7 +8182,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8041,7 +8601,7 @@ dependencies = [ [[package]] name = "operator-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "alloy-provider 1.0.41", @@ -8050,10 +8610,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", @@ -8086,12 +8646,41 @@ dependencies = [ "zkm-verifier", ] +[[package]] +name = "operator-wrapper-proof" +version = "0.4.0" +dependencies = [ + "anyhow", + "bincode", + "bitcoin", + "bitcoin-light-client-circuit", + "clap", + "dotenv", + "hex", + "proof-builder", + "serde", + "sha2 0.10.9", + "tokio", + "tracing", + "tracing-subscriber 0.3.23", + "zkm-build", + "zkm-primitives", + "zkm-prover", + "zkm-sdk", +] + [[package]] name = "option-ext" 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 +9143,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 +9155,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" dependencies = [ "digest 0.10.7", - "hmac", + "hmac 0.12.1", ] [[package]] @@ -8974,6 +9563,18 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "poseidon2" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/poseidon2#1ab68f436e220df5870cd6e39006804d5c806279" +dependencies = [ + "cfg-if", + "p3-field", + "p3-koala-bear", + "p3-symmetric", + "zkm-primitives", +] + [[package]] name = "potential_utf" version = "0.1.5" @@ -9155,11 +9756,10 @@ 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", "serde", @@ -9167,19 +9767,17 @@ dependencies = [ "state-chain", "strum 0.26.3", "thiserror 1.0.69", - "zkm-prover", "zkm-sdk", - "zkm-verifier", ] [[package]] name = "proof-builder-rpc" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "anyhow", "axum 0.8.8", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-light-client-circuit", "clap", "client", @@ -9190,6 +9788,7 @@ dependencies = [ "header-chain-proof", "http-body-util", "operator-proof", + "operator-wrapper-proof", "prometheus-client", "proof-builder", "serde", @@ -9206,6 +9805,7 @@ dependencies = [ "util", "uuid 1.23.0", "watchtower-proof", + "zkm-sdk", ] [[package]] @@ -9275,7 +9875,7 @@ version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" dependencies = [ - "heck 0.5.0", + "heck 0.4.1", "itertools 0.14.0", "log", "multimap 0.10.1", @@ -9674,6 +10274,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 +11054,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 +11087,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 +11129,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" @@ -10576,7 +11191,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", @@ -10716,7 +11331,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10819,7 +11434,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10980,7 +11595,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 +11607,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 +11659,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 +11677,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 +11778,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 +11980,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 +12015,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" @@ -11564,7 +12191,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11579,7 +12206,7 @@ dependencies = [ "httparse", "log", "rand 0.8.5", - "sha1", + "sha1 0.10.6", ] [[package]] @@ -11596,6 +12223,25 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "soldering-host" +version = "1.1.0" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +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", + "tracing", + "verifiable-circuit-babe", + "zkm-build", + "zkm-sdk", +] + [[package]] name = "spin" version = "0.5.2" @@ -11611,6 +12257,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 +12381,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 +12420,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 +12473,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 +12497,13 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] [[package]] name = "state-chain-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus 1.0.41", "alloy-primitives", @@ -11903,10 +12555,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 +12683,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#aba71380457d798039111e6cc0fdf2e0718c6766" dependencies = [ "bytemuck", "byteorder", @@ -12224,7 +12876,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12242,7 +12894,7 @@ dependencies = [ "num-traits", "once_cell", "prost 0.13.5", - "ripemd", + "ripemd 0.1.3", "serde", "serde_bytes", "serde_json", @@ -12955,7 +13607,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 +13627,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 +13762,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 +13813,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 +13839,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "util" -version = "0.3.3" +version = "0.4.0" dependencies = [ - "bitcoin 0.32.8", + "bitcoin", "hex", ] @@ -13250,12 +13908,54 @@ dependencies = [ "time", ] +[[package]] +name = "verifiable-circuit-babe" +version = "0.0.1" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +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", + "ripemd 0.2.0", + "serde", + "sha2 0.10.9", + "zkm-sdk", + "zkm-zkvm", +] + +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "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 +14132,7 @@ dependencies = [ [[package]] name = "watchtower-proof" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "anyhow", @@ -13440,7 +14140,7 @@ dependencies = [ "ark-groth16", "ark-serialize 0.5.0", "bincode", - "bitcoin 0.32.8", + "bitcoin", "bitcoin-light-client-circuit", "borsh", "chrono", @@ -13579,7 +14279,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -14210,6 +14910,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 +15101,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", ] @@ -14432,7 +15138,7 @@ dependencies = [ [[package]] name = "zkm-build" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "anyhow", "cargo_metadata", @@ -14443,7 +15149,7 @@ dependencies = [ [[package]] name = "zkm-core-executor" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "anyhow", "bincode", @@ -14484,7 +15190,7 @@ dependencies = [ [[package]] name = "zkm-core-machine" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "bincode", "cfg-if", @@ -14536,7 +15242,7 @@ dependencies = [ [[package]] name = "zkm-cuda" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "bincode", "ctrlc", @@ -14554,7 +15260,7 @@ dependencies = [ [[package]] name = "zkm-curves" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "cfg-if", "curve25519-dalek", @@ -14578,7 +15284,7 @@ dependencies = [ [[package]] name = "zkm-derive" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "proc-macro2", "quote", @@ -14588,7 +15294,7 @@ dependencies = [ [[package]] name = "zkm-lib" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "bincode", "cfg-if", @@ -14601,7 +15307,7 @@ dependencies = [ [[package]] name = "zkm-primitives" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "bincode", "hex", @@ -14619,7 +15325,7 @@ dependencies = [ [[package]] name = "zkm-prover" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "anyhow", "bincode", @@ -14656,7 +15362,7 @@ dependencies = [ [[package]] name = "zkm-recursion-circuit" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "hashbrown 0.14.5", "itertools 0.13.0", @@ -14689,7 +15395,7 @@ dependencies = [ [[package]] name = "zkm-recursion-compiler" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "backtrace", "itertools 0.13.0", @@ -14710,7 +15416,7 @@ dependencies = [ [[package]] name = "zkm-recursion-core" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "backtrace", "ff 0.13.1", @@ -14747,7 +15453,7 @@ dependencies = [ [[package]] name = "zkm-recursion-derive" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "quote", "syn 1.0.109", @@ -14756,7 +15462,7 @@ dependencies = [ [[package]] name = "zkm-recursion-gnark-ffi" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "anyhow", "bincode", @@ -14780,7 +15486,7 @@ dependencies = [ [[package]] name = "zkm-sdk" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "alloy-primitives", "alloy-signer 1.8.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -14827,7 +15533,7 @@ dependencies = [ [[package]] name = "zkm-stark" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "arrayref", "hashbrown 0.14.5", @@ -14892,7 +15598,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", @@ -14905,7 +15611,7 @@ dependencies = [ [[package]] name = "zkm-zkvm" version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#c736a41cccd623427295f89b53306673adc89966" +source = "git+https://github.com/ProjectZKM/Ziren#53a8eff4e716d6091c2ced0401b7b9537535392a" dependencies = [ "bincode", "cfg-if", diff --git a/Cargo.toml b/Cargo.toml index 88256e8c..f8aab0ce 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,7 +8,7 @@ members = [ "node", "proof-builder-rpc", "crates/cbft-rpc", - "crates/bitvm2-ga", + "crates/bitvm-gc", "crates/store", "crates/client", "crates/util", @@ -17,18 +17,20 @@ members = [ "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", "circuits/watchtower-proof/host", "circuits/operator-proof/host", + "circuits/operator-wrapper-proof/host", "circuits/proof-builder", ] 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/KSlashh/BitVM.git", branch = "gc-v2" } +goat = { git = "https://github.com/KSlashh/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" @@ -46,6 +48,8 @@ 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-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 +59,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"] } @@ -89,6 +95,10 @@ 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-recursion-core = { git = "https://github.com/ProjectZKM/Ziren" } +zkm-stark = { git = "https://github.com/ProjectZKM/Ziren" } +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 +106,10 @@ 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/bitvm2-gc", branch = "feat/babe" } +garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } +soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } store = { path = "crates/store" } util = { path = "crates/util" } client = { path = "crates/client" } @@ -107,8 +120,10 @@ 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" } +operator-wrapper-proof = { path = "circuits/operator-wrapper-proof/host" } watchtower-proof = { path = "circuits/watchtower-proof/host" } proof-builder = { path = "circuits/proof-builder" } @@ -176,4 +191,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/circuits/README.md b/circuits/README.md index a740c67f..1f15c81a 100644 --- a/circuits/README.md +++ b/circuits/README.md @@ -242,10 +242,18 @@ export WATCHTOWER_CHALLENGE_INIT_TXID="e7723e03ac97172cf033e40d4b9d9c0e22efa7a41 RUST_LOG=info cargo run --package operator-proof --bin operator-proof -r -- --output "data/operator-proof/output.bin" ``` +Then wrap the operator proof before the operator sends assert-commit. The Challenge script validates +the wrapper proof public inputs: + +``` +bash run-operator-wrapper-proof.sh "data/operator-proof/output.bin" "data/operator-wrapper-proof/output.bin" +``` + * 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. +* operator-wrapper-proof: wraps operator proof and exposes `operator_vk_hash_raw`, raw 16-byte `graph_id`, and `genesis_sequencer_commit_txid` as public inputs for Challenge. * 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. diff --git a/circuits/commit-chain-proof/guest/Cargo.lock b/circuits/commit-chain-proof/guest/Cargo.lock index 51593fa1..460084d2 100644 --- a/circuits/commit-chain-proof/guest/Cargo.lock +++ b/circuits/commit-chain-proof/guest/Cargo.lock @@ -1256,7 +1256,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commit-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1271,7 +1271,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -2235,7 +2235,6 @@ dependencies = [ "commit-chain", "sha2 0.10.9", "tracing", - "zkm-verifier", "zkm-zkvm", ] @@ -5649,6 +5648,13 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/circuits/commit-chain-proof/guest/Cargo.toml b/circuits/commit-chain-proof/guest/Cargo.toml index 395813bd..8f2d2e57 100644 --- a/circuits/commit-chain-proof/guest/Cargo.toml +++ b/circuits/commit-chain-proof/guest/Cargo.toml @@ -12,8 +12,6 @@ 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 = { path = "../../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 72963ff6..400281e8 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -101,11 +101,10 @@ 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]); + let commitment = parse_commit_chain_commitment(&op_return_data); if let tendermint::Hash::Sha256(expected_hash) = sequencer_hash(&ci.sequencers) { - assert_eq!(expected_hash, sequencer_set_hash); + assert_eq!(expected_hash, commitment.sequencer_set_hash); } else { panic!("Invalid sequencer set hash"); } @@ -206,8 +205,7 @@ 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(); + let prev_output = decode_commit_chain_circuit_output(&public_inputs); ( CommitChainPrevProofType::PrevProof(prev_output), proof_bytes, @@ -317,8 +315,7 @@ 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); diff --git a/circuits/cron-header-chain-proof.sh b/circuits/cron-header-chain-proof.sh index 5307187a..f41bdf4c 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 5e94224f..b3e561bf 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 7fb52b0c02fd3ddf6c47f3182d4832b5867dc4cb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44 ucmY#jfPjnPE5(+XGI<_*^8QA^om+=jfAyGac28Gok?~1YQ>AtRkSYLSI}iB) diff --git a/circuits/header-chain-proof/guest/Cargo.lock b/circuits/header-chain-proof/guest/Cargo.lock index 6e9bc5a8..cd02d292 100644 --- a/circuits/header-chain-proof/guest/Cargo.lock +++ b/circuits/header-chain-proof/guest/Cargo.lock @@ -1011,7 +1011,6 @@ dependencies = [ "log", "sha2", "tracing", - "zkm-verifier", "zkm-zkvm", ] @@ -1110,14 +1109,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "bitcoin", "borsh", "crypto-bigint", "serde", "sha2", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -2770,6 +2769,13 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/circuits/header-chain-proof/guest/Cargo.toml b/circuits/header-chain-proof/guest/Cargo.toml index 42e819c2..fe58279e 100644 --- a/circuits/header-chain-proof/guest/Cargo.toml +++ b/circuits/header-chain-proof/guest/Cargo.toml @@ -14,8 +14,6 @@ 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 = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging diff --git a/circuits/operator-proof/guest/Cargo.lock b/circuits/operator-proof/guest/Cargo.lock index 2a543d93..f09ea595 100644 --- a/circuits/operator-proof/guest/Cargo.lock +++ b/circuits/operator-proof/guest/Cargo.lock @@ -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", @@ -1282,7 +1282,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commit-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1297,7 +1297,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -2273,10 +2273,10 @@ dependencies = [ "bitcoin-light-client-circuit", "commit-chain", "header-chain", + "hex", "sha2 0.10.9", "state-chain", "tracing", - "zkm-verifier", "zkm-zkvm", ] @@ -2427,14 +2427,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -5165,7 +5165,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -5186,7 +5186,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -5755,6 +5755,13 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/circuits/operator-proof/guest/Cargo.toml b/circuits/operator-proof/guest/Cargo.toml index 521f3c95..a3be8844 100644 --- a/circuits/operator-proof/guest/Cargo.toml +++ b/circuits/operator-proof/guest/Cargo.toml @@ -15,8 +15,6 @@ bitcoin-light-client-circuit = { path = "../../../crates/bitcoin-light-client-ci # 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 = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging @@ -28,6 +26,9 @@ 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" +[build-dependencies] +hex = "0.4.3" + [patch.crates-io] # Precompile patches @@ -42,4 +43,3 @@ alloy-primitives-v1-4-1 = { git = "https://github.com/ziren-patches/core.git", p 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" } #bitcoin = { git = "https://github.com/ProjectZKM/rust-bitcoin", branch = "patch-0.32.7" } - diff --git a/circuits/operator-proof/guest/build.rs b/circuits/operator-proof/guest/build.rs new file mode 100644 index 00000000..6263faaf --- /dev/null +++ b/circuits/operator-proof/guest/build.rs @@ -0,0 +1,68 @@ +use std::{env, fs, path::PathBuf}; + +const ENV_FIXED_WATCHTOWER_KEYS: &str = "FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS"; + +fn parse_xonly_key(raw: &str) -> Result<[u8; 32], String> { + let hex = raw.trim().strip_prefix("0x").unwrap_or(raw.trim()); + let bytes = hex::decode(hex).map_err(|err| format!("invalid x-only public key hex: {err}"))?; + bytes.try_into().map_err(|bytes: Vec| { + format!("x-only public key must be 32 bytes, got {}", bytes.len()) + }) +} + +fn fixed_watchtower_keys_from_env() -> Vec<[u8; 32]> { + let value = match env::var(ENV_FIXED_WATCHTOWER_KEYS) { + Ok(value) => value, + Err(_) => { + println!( + "cargo:warning={ENV_FIXED_WATCHTOWER_KEYS} is not set; building operator guest with an empty fixed watchtower list" + ); + return Vec::new(); + } + }; + let keys = value + .split(',') + .map(str::trim) + .filter(|key| !key.is_empty()) + .map(parse_xonly_key) + .collect::, _>>() + .unwrap_or_else(|err| panic!("invalid {ENV_FIXED_WATCHTOWER_KEYS}: {err}")); + + if keys.is_empty() { + println!( + "cargo:warning={ENV_FIXED_WATCHTOWER_KEYS} contains no keys; building operator guest with an empty fixed watchtower list" + ); + return Vec::new(); + } + if keys.len() > 256 { + panic!("{ENV_FIXED_WATCHTOWER_KEYS} contains {} keys, max 256", keys.len()); + } + keys +} + +fn main() { + println!("cargo:rerun-if-env-changed={ENV_FIXED_WATCHTOWER_KEYS}"); + + let keys = fixed_watchtower_keys_from_env(); + let mut generated = String::new(); + generated.push_str(&format!("pub const FIXED_WATCHTOWER_COUNT: usize = {};\n", keys.len())); + generated.push_str(&format!( + "pub const FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS: [[u8; 32]; {}] = [\n", + keys.len() + )); + for key in keys { + generated.push_str(" ["); + for (index, byte) in key.iter().enumerate() { + if index > 0 { + generated.push_str(", "); + } + generated.push_str(&format!("0x{byte:02x}")); + } + generated.push_str("],\n"); + } + generated.push_str("];\n"); + + let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); + fs::write(out_dir.join("fixed_watchtowers.rs"), generated) + .expect("failed to write fixed_watchtowers.rs"); +} diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index f5c6dbcd..960d8445 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -1,20 +1,20 @@ #![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::{Address, U256}; +use bitcoin::{ScriptBuf, Transaction, TxOut}; +use bitcoin_light_client_circuit::EthClientExecutorInput; use commit_chain::CommitChainCircuitInput; +use header_chain::{HeaderChainCircuitInput, SPV}; use state_chain::StateChainCircuitInput; -use bitcoin::{ScriptBuf, TxOut, Transaction}; +use std::str::FromStr; + +include!(concat!(env!("OUT_DIR"), "/fixed_watchtowers.rs")); 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(); + 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 @@ -27,25 +27,30 @@ pub fn main() { 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 actual_operator_vk_hash: [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, operator_vk_hash) = + bitcoin_light_client_circuit::propose_longest_chain( + included_watchtowers, + graph_id, + operator_genesis_sequencer_commit_txid, + watchtower_challenge_txns, + watchtower_challenge_txn_pubkey, + watchtower_challenge_txn_scripts, + watchtower_challenge_txn_prev_outs, + &FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS, + operator_header_chain, + operator_commit_chain, + operator_state_chain, + spv_ss_commit, + operator_committed_blockhash, + actual_operator_vk_hash, + ); - zkm_zkvm::io::commit(&output); + zkm_zkvm::io::commit(&btc_best_block_hash); + zkm_zkvm::io::commit(&constant); + zkm_zkvm::io::commit(&included_watchtowers); + zkm_zkvm::io::commit(&operator_vk_hash); } diff --git a/circuits/operator-proof/host/Cargo.toml b/circuits/operator-proof/host/Cargo.toml index 2bc6c649..18605494 100644 --- a/circuits/operator-proof/host/Cargo.toml +++ b/circuits/operator-proof/host/Cargo.toml @@ -50,7 +50,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 0c21839a..bd89ff46 100644 --- a/circuits/operator-proof/host/build.rs +++ b/circuits/operator-proof/host/build.rs @@ -1,4 +1,13 @@ use zkm_build::build_program; + +const ENV_FIXED_WATCHTOWER_KEYS: &str = "FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS"; + fn main() { + println!("cargo:rerun-if-env-changed={ENV_FIXED_WATCHTOWER_KEYS}"); + if std::env::var(ENV_FIXED_WATCHTOWER_KEYS).is_err() { + println!( + "cargo:warning={ENV_FIXED_WATCHTOWER_KEYS} is not set; building operator guest with an empty fixed watchtower list" + ); + } build_program("../guest"); } diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 4d2f0fc0..575b1172 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -6,31 +6,21 @@ use bitcoin::{ hashes::Hash, secp256k1::{PublicKey, XOnlyPublicKey}, }; +use bitcoin_light_client_circuit::{build_spv, zkm_vk_hash_to_raw}; +use bitcoin_script::script; 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)] @@ -93,9 +83,7 @@ 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; @@ -119,8 +107,14 @@ pub async fn fetch_target_block_and_watchtower_tx( Vec, Vec, )> { - let watchtower_challenge_txids: Vec<&str> = watchtower_challenge_txids.split(",").collect(); - let watchtower_public_keys: Vec<&str> = watchtower_public_keys.split(",").collect(); + let watchtower_challenge_txids: Vec<&str> = + watchtower_challenge_txids.split(",").filter(|s| !s.is_empty()).collect(); + let watchtower_public_keys: Vec<&str> = + watchtower_public_keys.split(",").filter(|s| !s.is_empty()).collect(); + anyhow::ensure!( + watchtower_challenge_txids.len() == watchtower_public_keys.len(), + "watchtower challenge txids and public keys must have equal lengths" + ); let btc_client = BTCClient::new(bitcoin_network, Some(&esplora_url)); let latest_sequencer_commit_txid = Txid::from_str(&latest_sequencer_commit_txid)?; @@ -241,46 +235,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 @@ -409,23 +363,6 @@ impl ProofBuilder for OperatorProofBuilder { } }; - 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(); @@ -463,6 +400,9 @@ impl ProofBuilder for OperatorProofBuilder { &bitcoin_block_headers, ); + let actual_operator_vk_hash = zkm_vk_hash_to_raw(self.verifying_key.bytes32().as_bytes()) + .map_err(anyhow::Error::msg)?; + // Generate the proofs let (proof, cycles, proving_time) = tracing::info_span!("generate proof").in_scope( || -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { @@ -484,9 +424,9 @@ impl ProofBuilder for OperatorProofBuilder { 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()); + stdin.write(&actual_operator_vk_hash); let elf_id = if ELF_ID.get().is_none() { ELF_ID @@ -528,7 +468,7 @@ impl ProofBuilder for OperatorProofBuilder { 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(); + let proof = bincode::serialize(&proof)?; std::fs::write(&format!("{}", output), proof)?; Ok((public_value_hex, proof_size)) } @@ -544,80 +484,6 @@ mod tests { 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]); - } - #[tokio::test] #[ignore = "local test"] async fn test_parse_operator_proof() { @@ -627,8 +493,14 @@ 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 operator_vk_hash = + bitcoin_light_client_circuit::zkm_vk_hash_to_raw(vk_hash.as_bytes()).unwrap(); + let a = bitcoin_light_client_circuit::decode_operator_public_outputs( + proof.public_values.as_slice(), + operator_vk_hash, + ) + .unwrap(); println!( "block hash: {:?}, constant: {:?}, included map: {:?}", hex::encode(a.btc_best_block_hash), @@ -636,7 +508,6 @@ 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) })) diff --git a/circuits/operator-wrapper-proof/guest/Cargo.lock b/circuits/operator-wrapper-proof/guest/Cargo.lock new file mode 100644 index 00000000..332cd093 --- /dev/null +++ b/circuits/operator-wrapper-proof/guest/Cargo.lock @@ -0,0 +1,6696 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addchain" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" +dependencies = [ + "num-bigint 0.3.3", + "num-integer", + "num-traits", +] + +[[package]] +name = "addr2line" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "alloy-chains" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84e0378e959aa6a885897522080a990e80eb317f1e9a222a604492ea50e13096" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "num_enum", + "serde", + "strum 0.27.2", +] + +[[package]] +name = "alloy-consensus" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-trie", + "alloy-tx-macros", + "auto_impl", + "c-kzg", + "derive_more 2.1.1", + "either", + "k256", + "once_cell", + "rand 0.8.6", + "secp256k1 0.30.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-consensus-any" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-eip2124" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "crc", + "serde", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eip2930" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "serde", +] + +[[package]] +name = "alloy-eip7702" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "borsh", + "k256", + "serde", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-eips" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-eip2124", + "alloy-eip2930", + "alloy-eip7702", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "auto_impl", + "c-kzg", + "derive_more 2.1.1", + "either", + "serde", + "serde_with", + "sha2 0.10.9", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-evm" +version = "0.23.0" +source = "git+https://github.com/ziren-patches/evm?branch=patch-0.23.0#3cc30123cc6abb1c0da7e20318202821cab5d663" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-hardforks", + "alloy-primitives", + "alloy-sol-types", + "anyhow", + "auto_impl", + "derive_more 2.1.1", + "revm", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-genesis" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "alloy-trie", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-hardforks" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83ba208044232d14d4adbfa77e57d6329f51bc1acc21f5667bb7db72d88a0831" +dependencies = [ + "alloy-chains", + "alloy-eip2124", + "alloy-primitives", + "auto_impl", + "dyn-clone", +] + +[[package]] +name = "alloy-json-abi" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-json-rpc" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "http", + "serde", + "serde_json", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "alloy-network" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-json-rpc", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rpc-types-any", + "alloy-rpc-types-eth", + "alloy-serde", + "alloy-signer", + "alloy-sol-types", + "async-trait", + "auto_impl", + "derive_more 2.1.1", + "futures-utils-wasm", + "serde", + "serde_json", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-network-primitives" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-serde", + "serde", +] + +[[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" +dependencies = [ + "alloy-rlp", + "bytes", + "cfg-if", + "const-hex", + "derive_more 2.1.1", + "foldhash 0.2.0", + "hashbrown 0.16.1", + "indexmap 2.14.0", + "itoa", + "k256", + "keccak-asm", + "paste", + "proptest", + "rand 0.9.4", + "ruint", + "rustc-hash", + "serde", + "sha3", + "tiny-keccak", + "zkm-zkvm", +] + +[[package]] +name = "alloy-rlp" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" +dependencies = [ + "alloy-rlp-derive", + "arrayvec", + "bytes", +] + +[[package]] +name = "alloy-rlp-derive" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-rpc-types" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-primitives", + "alloy-rpc-types-eth", + "alloy-serde", + "serde", +] + +[[package]] +name = "alloy-rpc-types-any" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-consensus-any", + "alloy-rpc-types-eth", + "alloy-serde", +] + +[[package]] +name = "alloy-rpc-types-debug" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-primitives", + "derive_more 2.1.1", + "serde", + "serde_with", +] + +[[package]] +name = "alloy-rpc-types-engine" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "derive_more 2.1.1", + "strum 0.27.2", +] + +[[package]] +name = "alloy-rpc-types-eth" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-consensus", + "alloy-consensus-any", + "alloy-eips", + "alloy-network-primitives", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "alloy-sol-types", + "itertools 0.14.0", + "serde", + "serde_json", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-serde" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-primitives", + "serde", + "serde_json", +] + +[[package]] +name = "alloy-signer" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-primitives", + "async-trait", + "auto_impl", + "either", + "elliptic-curve", + "k256", + "thiserror 2.0.18", +] + +[[package]] +name = "alloy-sol-macro" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" +dependencies = [ + "alloy-sol-macro-input", + "const-hex", + "heck", + "indexmap 2.14.0", + "proc-macro-error2", + "proc-macro2", + "quote", + "sha3", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" +dependencies = [ + "const-hex", + "dunce", + "heck", + "macro-string", + "proc-macro2", + "quote", + "syn 2.0.117", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" +dependencies = [ + "serde", + "winnow 0.7.15", +] + +[[package]] +name = "alloy-sol-types" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "serde", +] + +[[package]] +name = "alloy-trie" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "derive_more 2.1.1", + "nybbles", + "serde", + "smallvec", + "thiserror 2.0.18", + "tracing", +] + +[[package]] +name = "alloy-tx-macros" +version = "1.0.41" +source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" +dependencies = [ + "alloy-primitives", + "darling 0.21.3", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anstream" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "is_terminal_polyfill", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "anstyle-parse" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" +dependencies = [ + "anstyle", + "once_cell_polyfill", + "windows-sys", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "ark-bls12-381" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-poly", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" +dependencies = [ + "ark-ff-asm 0.3.0", + "ark-ff-macros 0.3.0", + "ark-serialize 0.3.0", + "ark-std 0.3.0", + "derivative", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rustc_version 0.3.3", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" +dependencies = [ + "ark-ff-asm 0.4.2", + "ark-ff-macros 0.4.2", + "ark-serialize 0.4.2", + "ark-std 0.4.0", + "derivative", + "digest 0.10.7", + "itertools 0.10.5", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rustc_version 0.4.1", + "zeroize", +] + +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "paste", + "zeroize", +] + +[[package]] +name = "ark-ff-asm" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-ff-macros" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.5", +] + +[[package]] +name = "ark-serialize" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" +dependencies = [ + "ark-std 0.3.0", + "digest 0.9.0", +] + +[[package]] +name = "ark-serialize" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" +dependencies = [ + "ark-std 0.4.0", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.5.0", + "arrayvec", + "digest 0.10.7", + "num-bigint 0.4.6", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "ark-std" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "aurora-engine-modexp" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" +dependencies = [ + "hex", + "num", +] + +[[package]] +name = "auto_impl" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "backtrace" +version = "0.3.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "serde", + "windows-link", +] + +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + +[[package]] +name = "base58ck" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" +dependencies = [ + "bitcoin-internals", + "bitcoin_hashes", +] + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bech32" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitcoin" +version = "0.32.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" +dependencies = [ + "base58ck", + "bech32", + "bitcoin-internals", + "bitcoin-io", + "bitcoin-units", + "bitcoin_hashes", + "hex-conservative", + "hex_lit", + "secp256k1 0.29.0", + "serde", +] + +[[package]] +name = "bitcoin-internals" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" +dependencies = [ + "serde", +] + +[[package]] +name = "bitcoin-io" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" + +[[package]] +name = "bitcoin-light-client-circuit" +version = "0.4.0" +dependencies = [ + "alloy-primitives", + "base64 0.21.7", + "bincode", + "bitcoin", + "commit-chain", + "guest-executor", + "header-chain", + "hex", + "prost-build", + "revm-database-interface", + "serde", + "serde_json", + "state-chain", + "tendermint", + "tendermint-light-client-verifier", + "tracing", + "verifier", + "zkm-primitives", + "zkm-verifier", + "zkm-zkvm", +] + +[[package]] +name = "bitcoin-units" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118" +dependencies = [ + "bitcoin-internals", + "serde", +] + +[[package]] +name = "bitcoin_hashes" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" +dependencies = [ + "bitcoin-io", + "hex-conservative", + "serde", +] + +[[package]] +name = "bitflags" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" +dependencies = [ + "serde_core", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "serde", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "blake2b_simd" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" +dependencies = [ + "arrayref", + "arrayvec", + "constant_time_eq", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "bls12_381" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941" +dependencies = [ + "ff 0.12.1", + "group 0.12.1", + "pairing 0.22.0", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "bls12_381" +version = "0.8.0" +source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#bcf9397426410acc171ad625416b508dc913be8e" +dependencies = [ + "cfg-if", + "ff 0.13.1", + "group 0.13.0", + "pairing 0.23.0", + "rand_core 0.6.4", + "subtle", + "zkm-lib", +] + +[[package]] +name = "blst" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" +dependencies = [ + "cc", + "glob", + "threadpool", + "zeroize", +] + +[[package]] +name = "borsh" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" +dependencies = [ + "borsh-derive", + "bytes", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "byte-slice-cast" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" + +[[package]] +name = "bytemuck" +version = "1.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" +dependencies = [ + "serde", +] + +[[package]] +name = "c-kzg" +version = "2.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" +dependencies = [ + "blst", + "cc", + "glob", + "hex", + "libc", + "once_cell", + "serde", +] + +[[package]] +name = "cc" +version = "1.2.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" +dependencies = [ + "find-msvc-tools", + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "serde", + "windows-link", +] + +[[package]] +name = "colorchoice" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" + +[[package]] +name = "commit-chain" +version = "0.4.0" +dependencies = [ + "alloy-primitives", + "base64 0.21.7", + "bincode", + "bitcoin", + "guest-executor", + "hex", + "prost-build", + "revm-database-interface", + "serde", + "sha2 0.10.9", + "tendermint", + "tendermint-light-client-verifier", + "tracing", + "verifier", + "zkm-zkvm", +] + +[[package]] +name = "const-hex" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" +dependencies = [ + "cfg-if", + "cpufeatures", + "proptest", + "serde_core", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const_format" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" +dependencies = [ + "const_format_proc_macros", + "konst", +] + +[[package]] +name = "const_format_proc_macros" +version = "0.2.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "constant_time_eq" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" + +[[package]] +name = "convert_case" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cosmos-sdk-proto" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" +dependencies = [ + "informalsystems-pbjson", + "prost", + "serde", + "tendermint-proto 0.40.4", +] + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" + +[[package]] +name = "critical-section" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array 0.14.9", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.9", + "typenum", +] + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "fiat-crypto", + "rustc_version 0.4.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "curve25519-dalek-ng" +version = "4.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.6.4", + "subtle-ng", + "zeroize", +] + +[[package]] +name = "darling" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" +dependencies = [ + "darling_core 0.21.3", + "darling_macro 0.21.3", +] + +[[package]] +name = "darling" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" +dependencies = [ + "darling_core 0.23.0", + "darling_macro 0.23.0", +] + +[[package]] +name = "darling_core" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "serde", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_core" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" +dependencies = [ + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" +dependencies = [ + "darling_core 0.21.3", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "darling_macro" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" +dependencies = [ + "darling_core 0.23.0", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dashu" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85b3e5ac1e23ff1995ef05b912e2b012a8784506987a2651552db2c73fb3d7e0" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "dashu-macros", + "dashu-ratio", + "rustversion", +] + +[[package]] +name = "dashu-base" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289" + +[[package]] +name = "dashu-float" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85078445a8dbd2e1bd21f04a816f352db8d333643f0c9b78ca7c3d1df71063e7" +dependencies = [ + "dashu-base", + "dashu-int", + "num-modular", + "num-order", + "rustversion", + "static_assertions", +] + +[[package]] +name = "dashu-int" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf" +dependencies = [ + "cfg-if", + "dashu-base", + "num-modular", + "num-order", + "rustversion", + "static_assertions", +] + +[[package]] +name = "dashu-macros" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93381c3ef6366766f6e9ed9cf09e4ef9dec69499baf04f0c60e70d653cf0ab10" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "dashu-ratio", + "paste", + "proc-macro2", + "quote", + "rustversion", +] + +[[package]] +name = "dashu-ratio" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e33b04dd7ce1ccf8a02a69d3419e354f2bbfdf4eb911a0b7465487248764c9" +dependencies = [ + "dashu-base", + "dashu-float", + "dashu-int", + "num-modular", + "num-order", + "rustversion", +] + +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-where" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "0.99.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl 1.0.0", +] + +[[package]] +name = "derive_more" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" +dependencies = [ + "derive_more-impl 2.1.1", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "derive_more-impl" +version = "2.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "syn 2.0.117", + "unicode-xid", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.9", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer 0.10.4", + "const-oid", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "dyn-clone" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" + +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "git+https://github.com/ziren-patches/signatures.git?branch=patch-ecdsa-0.16.9#da0cd8753b243e09acf82f8e58f8da0e28e7287d" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "serdect", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-consensus" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" +dependencies = [ + "curve25519-dalek-ng", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +dependencies = [ + "serde", +] + +[[package]] +name = "elf" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" + +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff 0.13.1", + "generic-array 0.14.9", + "group 0.13.0", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "enum-map" +version = "2.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" +dependencies = [ + "enum-map-derive", + "serde", +] + +[[package]] +name = "enum-map-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "env_filter" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" +dependencies = [ + "log", + "regex", +] + +[[package]] +name = "env_logger" +version = "0.11.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" +dependencies = [ + "anstream", + "anstyle", + "env_filter", + "jiff", + "log", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys", +] + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + +[[package]] +name = "fastrlp" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "fastrlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", +] + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "bitvec", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "bitvec", + "byteorder", + "ff_derive", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "ff_derive" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60" +dependencies = [ + "addchain", + "num-bigint 0.3.3", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.6", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "flex-error" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" +dependencies = [ + "paste", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "foldhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-io" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-sink", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "futures-utils-wasm" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" + +[[package]] +name = "gcd" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" + +[[package]] +name = "generic-array" +version = "0.14.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" +dependencies = [ + "typenum", + "version_check", + "zeroize", +] + +[[package]] +name = "generic-array" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" +dependencies = [ + "rustversion", + "serde_core", + "typenum", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "libc", + "r-efi 5.3.0", + "wasip2", +] + +[[package]] +name = "getrandom" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", + "wasip2", + "wasip3", +] + +[[package]] +name = "gimli" +version = "0.32.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" + +[[package]] +name = "glob" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff 0.12.1", + "memuse", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff 0.13.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "guest" +version = "1.1.0" +dependencies = [ + "bitcoin", + "bitcoin-light-client-circuit", + "hex", + "tracing", + "verifier", + "zkm-primitives", + "zkm-zkvm", +] + +[[package]] +name = "guest-executor" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +dependencies = [ + "alloy-consensus", + "alloy-evm", + "alloy-network", + "alloy-primitives", + "alloy-rpc-types", + "bincode", + "itertools 0.13.0", + "kzg-rs", + "mpt", + "primitives", + "reth-chainspec", + "reth-consensus", + "reth-consensus-common", + "reth-errors", + "reth-ethereum-consensus", + "reth-ethereum-primitives", + "reth-evm", + "reth-evm-ethereum", + "reth-execution-types", + "reth-primitives-traits", + "reth-trie", + "revm", + "revm-primitives", + "serde", + "serde_json", + "serde_with", + "thiserror 1.0.69", +] + +[[package]] +name = "halo2" +version = "0.1.0-beta.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a23c779b38253fe1538102da44ad5bd5378495a61d2c4ee18d64eaa61ae5995" +dependencies = [ + "halo2_proofs", +] + +[[package]] +name = "halo2_proofs" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e925780549adee8364c7f2b685c753f6f3df23bde520c67416e93bf615933760" +dependencies = [ + "blake2b_simd", + "ff 0.12.1", + "group 0.12.1", + "pasta_curves 0.4.1", + "rand_core 0.6.4", + "rayon", +] + +[[package]] +name = "halo2curves" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d380afeef3f1d4d3245b76895172018cfb087d9976a7cabcd5597775b2933e07" +dependencies = [ + "blake2", + "digest 0.10.7", + "ff 0.13.1", + "group 0.13.0", + "halo2derive", + "hex", + "lazy_static", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "pairing 0.23.0", + "pasta_curves 0.5.1", + "paste", + "rand 0.8.6", + "rand_core 0.6.4", + "rayon", + "serde", + "serde_arrays 0.1.0", + "sha2 0.10.9", + "static_assertions", + "subtle", + "unroll", +] + +[[package]] +name = "halo2derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb99e7492b4f5ff469d238db464131b86c2eaac814a78715acba369f64d2c76" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "allocator-api2", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "allocator-api2", + "foldhash 0.1.5", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", + "serde", + "serde_core", +] + +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + +[[package]] +name = "header-chain" +version = "0.4.0" +dependencies = [ + "bitcoin", + "borsh", + "crypto-bigint", + "serde", + "sha2 0.10.9", + "verifier", + "zkm-zkvm", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-conservative" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "hex_lit" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" + +[[package]] +name = "hkdf" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" +dependencies = [ + "hmac", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.62.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" +dependencies = [ + "displaydoc", + "potential_utf", + "utf8_iter", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" +dependencies = [ + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" + +[[package]] +name = "icu_properties" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" +dependencies = [ + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" + +[[package]] +name = "icu_provider" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" +dependencies = [ + "displaydoc", + "icu_locale_core", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "id-arena" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.0", + "serde", + "serde_core", +] + +[[package]] +name = "informalsystems-pbjson" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aa4a0980c8379295100d70854354e78df2ee1c6ca0f96ffe89afeb3140e3a3d" +dependencies = [ + "base64 0.21.7", + "serde", +] + +[[package]] +name = "is_terminal_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "jiff" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" +dependencies = [ + "jiff-static", + "log", + "portable-atomic", + "portable-atomic-util", + "serde_core", +] + +[[package]] +name = "jiff-static" +version = "0.2.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "jobserver" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" +dependencies = [ + "getrandom 0.3.4", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.97" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jubjub" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a575df5f985fe1cd5b2b05664ff6accfc46559032b954529fd225a2168d27b0f" +dependencies = [ + "bitvec", + "bls12_381 0.7.1", + "ff 0.12.1", + "group 0.12.1", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "k256" +version = "0.13.4" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#8266b228a39402a0ba68d644b7f26b85b5112fe3" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "hex", + "once_cell", + "serdect", + "sha2 0.10.9", + "signature", + "zkm-lib", +] + +[[package]] +name = "keccak" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-asm" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa468878266ad91431012b3e5ef1bf9b170eab22883503a318d46857afa4579a" +dependencies = [ + "digest 0.10.7", + "sha3-asm", +] + +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + +[[package]] +name = "kzg-rs" +version = "0.2.6" +source = "git+https://github.com/ziren-patches/kzg-rs?branch=ziren#4cd5e3086a49dc1e94ffbd63ee4a7a9b2e342faa" +dependencies = [ + "bls12_381 0.8.0", + "ff 0.13.1", + "hex", + "serde_arrays 0.2.0", + "sha2 0.10.9", + "spin", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] + +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + +[[package]] +name = "litemap" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "macro-string" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "memuse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "modular-bitfield" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" +dependencies = [ + "modular-bitfield-impl", + "static_assertions", +] + +[[package]] +name = "modular-bitfield-impl" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "mpt" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types", + "alloy-rpc-types-debug", + "reth-trie", + "rlp", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "multimap" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "ntapi" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" +dependencies = [ + "winapi", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" +dependencies = [ + "num-bigint 0.4.6", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-conv" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-modular" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" + +[[package]] +name = "num-order" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" +dependencies = [ + "num-modular", +] + +[[package]] +name = "num-rational" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "num_enum" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" +dependencies = [ + "num_enum_derive", + "rustversion", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "nums" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf3c74f925fb8cfc49a8022f2afce48a0683b70f9e439885594e84c5edbf5b01" +dependencies = [ + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "rand 0.8.6", +] + +[[package]] +name = "nybbles" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" +dependencies = [ + "alloy-rlp", + "cfg-if", + "proptest", + "ruint", + "serde", + "smallvec", +] + +[[package]] +name = "object" +version = "0.37.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" +dependencies = [ + "critical-section", + "portable-atomic", +] + +[[package]] +name = "once_cell_polyfill" +version = "1.70.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" + +[[package]] +name = "op-alloy-consensus" +version = "0.22.0" +source = "git+https://github.com/ziren-patches/op-alloy?branch=patch-0.22.0#20048eaa105b1c06ca4fca6108a047a2fda0c494" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-serde", + "derive_more 2.1.1", + "serde", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" +dependencies = [ + "ecdsa", + "elliptic-curve", + "hex", + "primeorder", + "sha2 0.10.9", + "zkm-lib", +] + +[[package]] +name = "p3-air" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "p3-field", + "p3-matrix", +] + +[[package]] +name = "p3-bn254-fr" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "ff 0.13.1", + "halo2curves", + "num-bigint 0.4.6", + "p3-field", + "p3-poseidon2", + "p3-symmetric", + "rand 0.8.6", + "serde", +] + +[[package]] +name = "p3-challenger" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "p3-field", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "serde", + "tracing", +] + +[[package]] +name = "p3-circle" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-challenger", + "p3-commit", + "p3-dft", + "p3-field", + "p3-fri", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "serde", + "tracing", +] + +[[package]] +name = "p3-commit" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-challenger", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-util", + "serde", +] + +[[package]] +name = "p3-dft" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-field" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "nums", + "p3-maybe-rayon", + "p3-util", + "rand 0.8.6", + "serde", + "tracing", +] + +[[package]] +name = "p3-fri" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-challenger", + "p3-commit", + "p3-dft", + "p3-field", + "p3-interpolation", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "rand 0.8.6", + "serde", + "tracing", +] + +[[package]] +name = "p3-interpolation" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", +] + +[[package]] +name = "p3-keccak" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-field", + "p3-symmetric", + "p3-util", + "tiny-keccak", +] + +[[package]] +name = "p3-keccak-air" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "p3-air", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "tracing", +] + +[[package]] +name = "p3-koala-bear" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "p3-field", + "p3-mds", + "p3-monty-31", + "p3-poseidon2", + "p3-symmetric", + "rand 0.8.6", + "serde", +] + +[[package]] +name = "p3-matrix" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-field", + "p3-maybe-rayon", + "p3-util", + "rand 0.8.6", + "serde", + "tracing", + "transpose", +] + +[[package]] +name = "p3-maybe-rayon" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "rayon", +] + +[[package]] +name = "p3-mds" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-symmetric", + "p3-util", + "rand 0.8.6", +] + +[[package]] +name = "p3-merkle-tree" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-commit", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-symmetric", + "p3-util", + "rand 0.8.6", + "serde", + "tracing", +] + +[[package]] +name = "p3-mersenne-31" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "num-bigint 0.4.6", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "rand 0.8.6", + "serde", +] + +[[package]] +name = "p3-monty-31" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "num-bigint 0.4.6", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "rand 0.8.6", + "serde", + "serde_json", + "tracing", + "transpose", +] + +[[package]] +name = "p3-poseidon2" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "gcd", + "p3-field", + "p3-mds", + "p3-symmetric", + "rand 0.8.6", + "serde", +] + +[[package]] +name = "p3-symmetric" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-field", + "serde", +] + +[[package]] +name = "p3-uni-stark" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "itertools 0.13.0", + "p3-air", + "p3-challenger", + "p3-commit", + "p3-dft", + "p3-field", + "p3-matrix", + "p3-maybe-rayon", + "p3-util", + "serde", + "tracing", +] + +[[package]] +name = "p3-util" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" +dependencies = [ + "lock_api", + "serde", +] + +[[package]] +name = "pairing" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" +dependencies = [ + "group 0.12.1", +] + +[[package]] +name = "pairing" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" +dependencies = [ + "group 0.13.0", +] + +[[package]] +name = "parity-scale-codec" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" +dependencies = [ + "arrayvec", + "bitvec", + "byte-slice-cast", + "const_format", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "rustversion", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "pasta_curves" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5cc65faf8e7313b4b1fbaa9f7ca917a0eed499a9663be71477f87993604341d8" +dependencies = [ + "blake2b_simd", + "ff 0.12.1", + "group 0.12.1", + "lazy_static", + "rand 0.8.6", + "static_assertions", + "subtle", +] + +[[package]] +name = "pasta_curves" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" +dependencies = [ + "blake2b_simd", + "ff 0.13.1", + "group 0.13.0", + "hex", + "lazy_static", + "rand 0.8.6", + "serde", + "static_assertions", + "subtle", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pest" +version = "2.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" +dependencies = [ + "memchr", + "ucd-trie", +] + +[[package]] +name = "petgraph" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" +dependencies = [ + "fixedbitset", + "indexmap 2.14.0", +] + +[[package]] +name = "phf" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" +dependencies = [ + "phf_macros", + "phf_shared", + "serde", +] + +[[package]] +name = "phf_generator" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" +dependencies = [ + "fastrand", + "phf_shared", +] + +[[package]] +name = "phf_macros" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "phf_shared" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" +dependencies = [ + "siphasher", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "potential_utf" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "prettyplease" +version = "0.2.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" +dependencies = [ + "proc-macro2", + "syn 2.0.117", +] + +[[package]] +name = "primeorder" +version = "0.13.1" +source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" +dependencies = [ + "elliptic-curve", +] + +[[package]] +name = "primitive-types" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" +dependencies = [ + "fixed-hash", + "impl-codec", + "uint", +] + +[[package]] +name = "primitives" +version = "0.1.0" +source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" +dependencies = [ + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-rpc-types", + "eyre", + "reth-chainspec", + "reth-primitives-traits", + "reth-trie", + "serde", + "serde_json", +] + +[[package]] +name = "proc-macro-crate" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags", + "num-traits", + "rand 0.9.4", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "prost" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-build" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" +dependencies = [ + "heck", + "itertools 0.14.0", + "log", + "multimap", + "once_cell", + "petgraph", + "prettyplease", + "prost", + "prost-types", + "regex", + "syn 2.0.117", + "tempfile", +] + +[[package]] +name = "prost-derive" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" +dependencies = [ + "anyhow", + "itertools 0.14.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "prost-types" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" +dependencies = [ + "prost", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", + "serde", +] + +[[package]] +name = "rand" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.5", + "serde", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.5", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.17", +] + +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" +dependencies = [ + "getrandom 0.3.4", + "serde", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + +[[package]] +name = "rayon" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "rayon-scan" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f87cc11a0140b4b0da0ffc889885760c61b13672d80a908920b2c0df078fa14" +dependencies = [ + "rayon", +] + +[[package]] +name = "ref-cast" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" + +[[package]] +name = "reth-chainspec" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-chains", + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-genesis", + "alloy-primitives", + "alloy-trie", + "auto_impl", + "derive_more 2.1.1", + "reth-ethereum-forks", + "reth-network-peers", + "reth-primitives-traits", + "serde_json", +] + +[[package]] +name = "reth-codecs" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-trie", + "bytes", + "modular-bitfield", + "op-alloy-consensus", + "reth-codecs-derive", + "reth-zstd-compressors", + "serde", +] + +[[package]] +name = "reth-codecs-derive" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "reth-consensus" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "auto_impl", + "reth-execution-types", + "reth-primitives-traits", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-consensus-common" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "reth-chainspec", + "reth-consensus", + "reth-primitives-traits", +] + +[[package]] +name = "reth-db-models" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "reth-primitives-traits", +] + +[[package]] +name = "reth-errors" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "reth-consensus", + "reth-execution-errors", + "reth-storage-errors", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-ethereum-consensus" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "reth-chainspec", + "reth-consensus", + "reth-consensus-common", + "reth-execution-types", + "reth-primitives-traits", + "tracing", +] + +[[package]] +name = "reth-ethereum-forks" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-eip2124", + "alloy-hardforks", + "alloy-primitives", + "auto_impl", + "once_cell", +] + +[[package]] +name = "reth-ethereum-primitives" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-serde", + "reth-codecs", + "reth-primitives-traits", + "serde", + "serde_with", +] + +[[package]] +name = "reth-evm" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-primitives", + "anyhow", + "auto_impl", + "derive_more 2.1.1", + "futures-util", + "reth-execution-errors", + "reth-execution-types", + "reth-primitives-traits", + "reth-storage-api", + "reth-storage-errors", + "reth-trie-common", + "revm", + "revm-database", +] + +[[package]] +name = "reth-evm-ethereum" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-primitives", + "alloy-rpc-types-engine", + "reth-chainspec", + "reth-ethereum-forks", + "reth-ethereum-primitives", + "reth-evm", + "reth-execution-types", + "reth-primitives-traits", + "reth-storage-errors", + "revm", +] + +[[package]] +name = "reth-execution-errors" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-evm", + "alloy-primitives", + "alloy-rlp", + "nybbles", + "reth-storage-errors", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-execution-types" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-evm", + "alloy-primitives", + "derive_more 2.1.1", + "reth-ethereum-primitives", + "reth-primitives-traits", + "reth-trie-common", + "revm", +] + +[[package]] +name = "reth-network-peers" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "serde_with", + "thiserror 2.0.18", + "url", +] + +[[package]] +name = "reth-primitives-traits" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-genesis", + "alloy-primitives", + "alloy-rlp", + "alloy-rpc-types-eth", + "alloy-trie", + "auto_impl", + "bytes", + "derive_more 2.1.1", + "once_cell", + "op-alloy-consensus", + "reth-codecs", + "revm-bytecode", + "revm-primitives", + "revm-state", + "secp256k1 0.30.0", + "serde", + "serde_with", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-prune-types" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-primitives", + "derive_more 2.1.1", + "strum 0.27.2", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-stages-types" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-primitives", + "reth-trie-common", +] + +[[package]] +name = "reth-static-file-types" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-primitives", + "derive_more 2.1.1", + "serde", + "strum 0.27.2", +] + +[[package]] +name = "reth-storage-api" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rpc-types-engine", + "auto_impl", + "reth-chainspec", + "reth-db-models", + "reth-ethereum-primitives", + "reth-execution-types", + "reth-primitives-traits", + "reth-prune-types", + "reth-stages-types", + "reth-storage-errors", + "reth-trie-common", + "revm-database", +] + +[[package]] +name = "reth-storage-errors" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "derive_more 2.1.1", + "reth-primitives-traits", + "reth-prune-types", + "reth-static-file-types", + "revm-database-interface", + "thiserror 2.0.18", +] + +[[package]] +name = "reth-trie" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-eips", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "auto_impl", + "itertools 0.14.0", + "reth-execution-errors", + "reth-primitives-traits", + "reth-stages-types", + "reth-storage-errors", + "reth-trie-common", + "reth-trie-sparse", + "revm-database", + "tracing", +] + +[[package]] +name = "reth-trie-common" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "derive_more 2.1.1", + "itertools 0.14.0", + "nybbles", + "rayon", + "reth-primitives-traits", + "revm-database", +] + +[[package]] +name = "reth-trie-sparse" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "alloy-primitives", + "alloy-rlp", + "alloy-trie", + "auto_impl", + "reth-execution-errors", + "reth-primitives-traits", + "reth-trie-common", + "smallvec", + "tracing", +] + +[[package]] +name = "reth-zstd-compressors" +version = "1.9.3" +source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" +dependencies = [ + "zstd", +] + +[[package]] +name = "revm" +version = "31.0.2" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database", + "revm-database-interface", + "revm-handler", + "revm-inspector", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", +] + +[[package]] +name = "revm-bytecode" +version = "7.1.1" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "bitvec", + "phf", + "revm-primitives", + "serde", +] + +[[package]] +name = "revm-context" +version = "11.0.2" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "alloy-consensus", + "bitvec", + "cfg-if", + "derive-where", + "revm-bytecode", + "revm-context-interface", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-context-interface" +version = "12.0.1" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "alloy-consensus", + "alloy-eip2930", + "alloy-eip7702", + "auto_impl", + "either", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-database" +version = "9.0.5" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "alloy-eips", + "revm-bytecode", + "revm-database-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-database-interface" +version = "8.0.5" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "auto_impl", + "either", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-handler" +version = "12.0.2" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "auto_impl", + "derive-where", + "revm-bytecode", + "revm-context", + "revm-context-interface", + "revm-database-interface", + "revm-interpreter", + "revm-precompile", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-inspector" +version = "12.0.2" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "auto_impl", + "either", + "revm-context", + "revm-database-interface", + "revm-handler", + "revm-interpreter", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-interpreter" +version = "29.0.1" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "revm-bytecode", + "revm-context-interface", + "revm-primitives", + "revm-state", + "serde", +] + +[[package]] +name = "revm-precompile" +version = "29.0.1" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "ark-bls12-381", + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "arrayref", + "aurora-engine-modexp", + "cfg-if", + "k256", + "p256", + "revm-primitives", + "ripemd", + "sha2 0.10.9", + "substrate-bn", +] + +[[package]] +name = "revm-primitives" +version = "21.0.2" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "alloy-primitives", + "num_enum", + "once_cell", + "serde", +] + +[[package]] +name = "revm-state" +version = "8.1.1" +source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" +dependencies = [ + "bitflags", + "revm-bytecode", + "revm-primitives", + "serde", +] + +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "git+https://github.com/ziren-patches/signatures.git?branch=patch-ecdsa-0.16.9#da0cd8753b243e09acf82f8e58f8da0e28e7287d" +dependencies = [ + "hmac", + "subtle", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rustc-hex", +] + +[[package]] +name = "ruint" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" +dependencies = [ + "alloy-rlp", + "ark-ff 0.3.0", + "ark-ff 0.4.2", + "ark-ff 0.5.0", + "bytes", + "fastrlp 0.3.1", + "fastrlp 0.4.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "parity-scale-codec", + "primitive-types", + "proptest", + "rand 0.8.6", + "rand 0.9.4", + "rlp", + "ruint-macro", + "serde_core", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-demangle" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" + +[[package]] +name = "rustc-hash" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" +dependencies = [ + "semver 0.11.0", +] + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver 1.0.28", +] + +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "scale-info" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" +dependencies = [ + "cfg-if", + "derive_more 1.0.0", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "schemars" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "schemars" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" +dependencies = [ + "dyn-clone", + "ref-cast", + "serde", + "serde_json", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.9", + "pkcs8", + "serdect", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" +dependencies = [ + "bitcoin_hashes", + "rand 0.8.6", + "secp256k1-sys", + "serde", +] + +[[package]] +name = "secp256k1-sys" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" +dependencies = [ + "cc", +] + +[[package]] +name = "semver" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" +dependencies = [ + "semver-parser", +] + +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + +[[package]] +name = "semver-parser" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" +dependencies = [ + "pest", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_arrays" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_arrays" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_repr" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serde_with" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.14.0", + "schemars 0.9.0", + "schemars 1.2.1", + "serde_core", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" +dependencies = [ + "darling 0.23.0", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "git+https://github.com/ziren-patches/RustCrypto-hashes?branch=patch-sha2-0.10.9#dbfdbd088ac7b55e03c0b9eb192278efe35d67dc" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.7", +] + +[[package]] +name = "sha3" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" +dependencies = [ + "digest 0.10.7", + "keccak", +] + +[[package]] +name = "sha3-asm" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59cbb88c189d6352cc8ae96a39d19c7ecad8f7330b29461187f2587fdc2988d5" +dependencies = [ + "cc", + "cfg-if", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest 0.10.7", + "rand_core 0.6.4", +] + +[[package]] +name = "siphasher" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" + +[[package]] +name = "size" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fed904c7fb2856d868b92464fc8fa597fce366edea1a9cbfaa8cb5fe080bd6d" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +dependencies = [ + "serde", +] + +[[package]] +name = "snowbridge-amcl" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460a9ed63cdf03c1b9847e8a12a5f5ba19c4efd5869e4a737e05be25d7c427e5" +dependencies = [ + "parity-scale-codec", + "scale-info", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" + +[[package]] +name = "state-chain" +version = "0.4.0" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "base64 0.21.7", + "bincode", + "bitcoin", + "cosmos-sdk-proto", + "guest-executor", + "header-chain", + "hex", + "prost", + "prost-build", + "prost-derive", + "revm-database-interface", + "serde", + "serde_json", + "sha2 0.10.9", + "tendermint", + "tendermint-light-client-verifier", + "tracing", + "verifier", + "zkm-zkvm", +] + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strength_reduce" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" + +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "strum" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" +dependencies = [ + "strum_macros 0.26.4", +] + +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros 0.27.2", +] + +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.117", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "substrate-bn" +version = "0.6.0" +source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" +dependencies = [ + "bytemuck", + "byteorder", + "cfg-if", + "crunchy", + "lazy_static", + "num-bigint 0.4.6", + "rand 0.8.6", + "rustc-hex", + "zkm-lib", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "subtle-encoding" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" +dependencies = [ + "zeroize", +] + +[[package]] +name = "subtle-ng" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "sysinfo" +version = "0.30.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "rayon", + "windows", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.2", + "once_cell", + "rustix", + "windows-sys", +] + +[[package]] +name = "tendermint" +version = "0.40.3" +source = "git+https://github.com/ProjectZKM/tendermint-rs?branch=patch-0.40.3#5bd318a89601f7db2bce05018246c5ef6dc7fb9b" +dependencies = [ + "bytes", + "digest 0.10.7", + "ed25519", + "ed25519-consensus", + "flex-error", + "futures", + "k256", + "num-traits", + "once_cell", + "prost", + "ripemd", + "serde", + "serde_bytes", + "serde_json", + "serde_repr", + "sha2 0.10.9", + "signature", + "subtle", + "subtle-encoding", + "tendermint-proto 0.40.3", + "time", + "zeroize", +] + +[[package]] +name = "tendermint-light-client-verifier" +version = "0.40.3" +source = "git+https://github.com/ProjectZKM/tendermint-rs?branch=patch-0.40.3#5bd318a89601f7db2bce05018246c5ef6dc7fb9b" +dependencies = [ + "derive_more 0.99.20", + "flex-error", + "serde", + "tendermint", + "time", +] + +[[package]] +name = "tendermint-proto" +version = "0.40.3" +source = "git+https://github.com/ProjectZKM/tendermint-rs?branch=patch-0.40.3#5bd318a89601f7db2bce05018246c5ef6dc7fb9b" +dependencies = [ + "bytes", + "flex-error", + "prost", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "tendermint-proto" +version = "0.40.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" +dependencies = [ + "bytes", + "flex-error", + "prost", + "serde", + "serde_bytes", + "subtle-encoding", + "time", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl 2.0.18", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "toml_datetime" +version = "1.1.1+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_edit" +version = "0.25.11+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" +dependencies = [ + "indexmap 2.14.0", + "toml_datetime", + "toml_parser", + "winnow 1.0.2", +] + +[[package]] +name = "toml_parser" +version = "1.1.2+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" +dependencies = [ + "winnow 1.0.2", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-forest" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee40835db14ddd1e3ba414292272eddde9dad04d3d4b65509656414d1c42592f" +dependencies = [ + "ansi_term", + "smallvec", + "thiserror 1.0.69", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "transpose" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" +dependencies = [ + "num-integer", + "strength_reduce", +] + +[[package]] +name = "typenum" +version = "1.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" + +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + +[[package]] +name = "unroll" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618" +dependencies = [ + "quote", + "syn 1.0.109", +] + +[[package]] +name = "url" +version = "2.5.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "utf8parse" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +dependencies = [ + "serde", +] + +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "zkm-verifier", +] + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasip2" +version = "1.0.3+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" +dependencies = [ + "wit-bindgen 0.57.1", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen 0.51.0", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.117", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.120" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.14.0", + "wasm-encoder", + "wasmparser", +] + +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags", + "hashbrown 0.15.5", + "indexmap 2.14.0", + "semver 1.0.28", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core 0.52.0", + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" +dependencies = [ + "memchr", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck", + "wit-parser", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.14.0", + "prettyplease", + "syn 2.0.117", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.117", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags", + "indexmap 2.14.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.14.0", + "log", + "semver 1.0.28", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", +] + +[[package]] +name = "writeable" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "yoke" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" +dependencies = [ + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.48" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerofrom" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zerotrie" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.117", +] + +[[package]] +name = "zkhash" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4352d1081da6922701401cdd4cbf29a2723feb4cfabb5771f6fee8e9276da1c7" +dependencies = [ + "ark-ff 0.4.2", + "ark-std 0.4.0", + "bitvec", + "blake2", + "bls12_381 0.7.1", + "byteorder", + "cfg-if", + "group 0.12.1", + "group 0.13.0", + "halo2", + "hex", + "jubjub", + "lazy_static", + "pasta_curves 0.5.1", + "rand 0.8.6", + "serde", + "sha2 0.10.9", + "sha3", + "subtle", +] + +[[package]] +name = "zkm-core-executor" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "anyhow", + "bincode", + "bytemuck", + "elf", + "enum-map", + "env_logger", + "eyre", + "hashbrown 0.14.5", + "hex", + "itertools 0.13.0", + "log", + "nohash-hasher", + "num", + "num_enum", + "p3-field", + "p3-koala-bear", + "p3-maybe-rayon", + "p3-symmetric", + "rand 0.8.6", + "rayon-scan", + "serde", + "serde_json", + "sha2 0.10.9", + "strum 0.26.3", + "strum_macros 0.26.4", + "thiserror 1.0.69", + "tiny-keccak", + "tracing", + "tracing-subscriber", + "typenum", + "vec_map", + "zkm-curves", + "zkm-primitives", + "zkm-stark", +] + +[[package]] +name = "zkm-core-machine" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "bincode", + "cfg-if", + "elliptic-curve", + "generic-array 1.4.1", + "hashbrown 0.14.5", + "hex", + "itertools 0.13.0", + "k256", + "log", + "num", + "num_cpus", + "p256", + "p3-air", + "p3-challenger", + "p3-field", + "p3-keccak-air", + "p3-koala-bear", + "p3-matrix", + "p3-maybe-rayon", + "p3-poseidon2", + "p3-uni-stark", + "p3-util", + "rand 0.8.6", + "rayon", + "rayon-scan", + "serde", + "serde_json", + "size", + "snowbridge-amcl", + "static_assertions", + "strum 0.26.3", + "strum_macros 0.26.4", + "tempfile", + "thiserror 1.0.69", + "tiny-keccak", + "tracing", + "tracing-forest", + "tracing-subscriber", + "typenum", + "web-time", + "zkm-core-executor", + "zkm-curves", + "zkm-derive", + "zkm-primitives", + "zkm-stark", +] + +[[package]] +name = "zkm-curves" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "cfg-if", + "curve25519-dalek", + "dashu", + "elliptic-curve", + "generic-array 1.4.1", + "itertools 0.13.0", + "k256", + "num", + "p256", + "p3-field", + "serde", + "snowbridge-amcl", + "thiserror 1.0.69", + "tracing", + "typenum", + "zkm-primitives", + "zkm-stark", +] + +[[package]] +name = "zkm-derive" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "zkm-lib" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "bincode", + "cfg-if", + "elliptic-curve", + "serde", + "sha2 0.10.9", + "zkm-primitives", +] + +[[package]] +name = "zkm-primitives" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "bincode", + "hex", + "lazy_static", + "num-bigint 0.4.6", + "p3-field", + "p3-koala-bear", + "p3-monty-31", + "p3-poseidon2", + "p3-symmetric", + "serde", + "sha2 0.10.9", +] + +[[package]] +name = "zkm-recursion-core" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "backtrace", + "ff 0.13.1", + "hashbrown 0.14.5", + "itertools 0.13.0", + "p3-air", + "p3-bn254-fr", + "p3-challenger", + "p3-commit", + "p3-dft", + "p3-field", + "p3-fri", + "p3-koala-bear", + "p3-matrix", + "p3-maybe-rayon", + "p3-merkle-tree", + "p3-monty-31", + "p3-poseidon2", + "p3-symmetric", + "p3-util", + "rand 0.8.6", + "serde", + "static_assertions", + "thiserror 1.0.69", + "tracing", + "vec_map", + "zkhash", + "zkm-core-machine", + "zkm-derive", + "zkm-primitives", + "zkm-stark", +] + +[[package]] +name = "zkm-stark" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "arrayref", + "hashbrown 0.14.5", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "p3-air", + "p3-challenger", + "p3-circle", + "p3-commit", + "p3-dft", + "p3-field", + "p3-fri", + "p3-keccak", + "p3-koala-bear", + "p3-matrix", + "p3-maybe-rayon", + "p3-mds", + "p3-merkle-tree", + "p3-mersenne-31", + "p3-poseidon2", + "p3-symmetric", + "p3-uni-stark", + "p3-util", + "rand 0.8.6", + "rayon-scan", + "serde", + "strum 0.26.3", + "strum_macros 0.26.4", + "sysinfo", + "tracing", + "tracing-forest", + "tracing-subscriber", + "zkm-derive", + "zkm-primitives", + "zkm-zkvm", +] + +[[package]] +name = "zkm-verifier" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "anyhow", + "bincode", + "hex", + "itertools 0.13.0", + "lazy_static", + "once_cell", + "p3-bn254-fr", + "p3-commit", + "p3-field", + "p3-koala-bear", + "p3-symmetric", + "p3-util", + "rayon", + "serde", + "sha2 0.10.9", + "strum_macros 0.26.4", + "substrate-bn", + "thiserror 2.0.18", + "zkm-core-executor", + "zkm-core-machine", + "zkm-primitives", + "zkm-recursion-core", + "zkm-stark", +] + +[[package]] +name = "zkm-zkvm" +version = "1.2.5" +source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" +dependencies = [ + "bincode", + "cfg-if", + "getrandom 0.2.17", + "lazy_static", + "libm", + "p3-field", + "p3-koala-bear", + "rand 0.8.6", + "serde", + "sha2 0.10.9", + "zkm-lib", + "zkm-primitives", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" + +[[package]] +name = "zstd" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.16+zstd.1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" +dependencies = [ + "cc", + "pkg-config", +] + +[[patch.unused]] +name = "alloy-primitives" +version = "1.0.0" +source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.0.0#1367f9d00583135013224d764b51d63ffcdd9145" + +[[patch.unused]] +name = "alloy-primitives" +version = "1.1.0" +source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.1.0#66386c419c93166f8471d3ace213d41ffcb13635" + +[[patch.unused]] +name = "alloy-primitives" +version = "1.1.2" +source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.1.2#288a6856fc6e4160e85903f2af1c4af92ed4ccf9" + +[[patch.unused]] +name = "secp256k1" +version = "0.29.1" +source = "git+https://github.com/ziren-patches/rust-secp256k1?branch=patch-0.29.1#436fc77c1c72fd151f11004bec618d993b9a2f00" diff --git a/circuits/operator-wrapper-proof/guest/Cargo.toml b/circuits/operator-wrapper-proof/guest/Cargo.toml new file mode 100644 index 00000000..e24a035a --- /dev/null +++ b/circuits/operator-wrapper-proof/guest/Cargo.toml @@ -0,0 +1,33 @@ +[workspace] + +[package] +name = "guest" +version = "1.1.0" +edition = "2021" +publish = false + +[dependencies] +bitcoin-light-client-circuit = { path = "../../../crates/bitcoin-light-client-circuit" } +verifier = { path = "../../../crates/verifier" } + +# Ziren +zkm-primitives = { git = "https://github.com/ProjectZKM/Ziren" } +zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } + +# Statically turns off logging +tracing = { version = "0.1", features = ["max_level_trace"] } + +bitcoin = { version = "0.32.6", features = ["serde", "rand", "secp-recovery"] } +hex = "0.4.3" + +[patch.crates-io] +# Precompile patches +sha2 = { git = "https://github.com/ziren-patches/RustCrypto-hashes", branch = "patch-sha2-0.10.9", package = "sha2" } +bn = { git = "https://github.com/ziren-patches/bn", branch = "patch-0.6.0", package = "substrate-bn" } +k256 = { git = "https://github.com/ziren-patches/elliptic-curves", branch = "patch-k256-0.13.4" } +p256 = { git = "https://github.com/ziren-patches/elliptic-curves", branch = "patch-p256-0.13.2" } +alloy-primitives-v1-0-0 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.0.0" } +alloy-primitives-v1-1-0 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.1.0" } +alloy-primitives-v1-1-2 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.1.2" } +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" } diff --git a/circuits/operator-wrapper-proof/guest/src/main.rs b/circuits/operator-wrapper-proof/guest/src/main.rs new file mode 100644 index 00000000..d4c5903a --- /dev/null +++ b/circuits/operator-wrapper-proof/guest/src/main.rs @@ -0,0 +1,39 @@ +#![no_main] +zkm_zkvm::entrypoint!(main); + +use bitcoin_light_client_circuit::{ + decode_operator_public_outputs, hash_operator_constant, zkm_vk_hash_to_raw, +}; +use verifier::verify_groth16_proof; + +pub fn main() { + let operator_proof: Vec = zkm_zkvm::io::read(); + let operator_public_values: Vec = zkm_zkvm::io::read(); + let operator_vk_hash: Vec = zkm_zkvm::io::read(); + let operator_zkm_version: String = zkm_zkvm::io::read(); + let graph_id: [u8; 16] = zkm_zkvm::io::read(); + let genesis_sequencer_commit_txid: [u8; 32] = zkm_zkvm::io::read(); + + verify_groth16_proof( + &operator_proof, + &operator_public_values, + &operator_vk_hash, + &operator_zkm_version, + ) + .expect("Failed to verify operator proof"); + + let expected_constant = hash_operator_constant(graph_id, genesis_sequencer_commit_txid); + let operator_vk_hash_raw = + zkm_vk_hash_to_raw(&operator_vk_hash).expect("Invalid operator vk hash"); + let operator_outputs = decode_operator_public_outputs( + &operator_public_values, + operator_vk_hash_raw, + ) + .expect("Invalid operator public values"); + assert_eq!(operator_outputs.constant, expected_constant); + assert_eq!(operator_outputs.operator_vk_hash, operator_vk_hash_raw); + + zkm_zkvm::io::commit(&operator_vk_hash_raw); + zkm_zkvm::io::commit(&graph_id); + zkm_zkvm::io::commit(&genesis_sequencer_commit_txid); +} diff --git a/circuits/operator-wrapper-proof/host/Cargo.toml b/circuits/operator-wrapper-proof/host/Cargo.toml new file mode 100644 index 00000000..c8eb5ded --- /dev/null +++ b/circuits/operator-wrapper-proof/host/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "operator-wrapper-proof" +version.workspace = true +edition.workspace = true + +[dependencies] +anyhow.workspace = true +bincode.workspace = true +bitcoin = { workspace = true, features = ["serde", "rand", "secp-recovery"] } +bitcoin-light-client-circuit = { workspace = true } +clap = { workspace = true, features = ["derive", "env"] } +dotenv.workspace = true +hex.workspace = true +proof-builder.workspace = true +serde.workspace = true +sha2.workspace = true +tokio = { workspace = true, features = ["full"] } +tracing = { workspace = true, features = ["max_level_trace"] } +tracing-subscriber.workspace = true +zkm-primitives.workspace = true +zkm-prover.workspace = true +zkm-sdk.workspace = true + +[build-dependencies] +zkm-build.workspace = true diff --git a/circuits/operator-wrapper-proof/host/build.rs b/circuits/operator-wrapper-proof/host/build.rs new file mode 100644 index 00000000..dcfa0c53 --- /dev/null +++ b/circuits/operator-wrapper-proof/host/build.rs @@ -0,0 +1,5 @@ +use zkm_build::build_program; + +fn main() { + build_program("../guest"); +} diff --git a/circuits/operator-wrapper-proof/host/src/lib.rs b/circuits/operator-wrapper-proof/host/src/lib.rs new file mode 100644 index 00000000..12e181a5 --- /dev/null +++ b/circuits/operator-wrapper-proof/host/src/lib.rs @@ -0,0 +1,302 @@ +//! Generate operator wrapper proof. +use anyhow::{Context, ensure}; +use bitcoin::{Txid, hashes::Hash}; +use bitcoin_light_client_circuit::{ + OperatorPublicOutputs, decode_operator_public_outputs, hash_operator_constant, + zkm_vk_hash_to_raw, +}; +use clap::Parser; +use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; +use sha2::{Digest, Sha256}; +use std::fs; +use std::str::FromStr; +use std::sync::OnceLock; +use zkm_sdk::{ + HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, + include_elf, +}; + +static ELF_ID: OnceLock = OnceLock::new(); +const OPERATOR_WRAPPER: &[u8] = include_elf!("guest"); + +#[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] +pub struct Args { + #[serde(default)] + #[arg(long, default_value_t = true)] + pub enable: bool, + + #[serde(default)] + #[clap(long, env, default_value = "")] + pub operator_input_proof: String, + + #[serde(default)] + #[clap(long, env, default_value = "")] + pub graph_id: String, + + #[serde(default)] + #[clap(long, env, default_value = "")] + pub genesis_sequencer_commit_txid: String, + + #[serde(default)] + #[clap(long, env, default_value = "wrapper-proof.bin")] + pub output: String, + + #[serde(default = "default_scan_interval")] + #[arg(long, default_value_t = 30)] + pub scan_interval: u64, +} + +fn default_scan_interval() -> u64 { + 30 +} + +impl Default for Args { + fn default() -> Self { + Self { + enable: false, + operator_input_proof: String::new(), + graph_id: String::new(), + genesis_sequencer_commit_txid: String::new(), + output: "wrapper-proof.bin".to_string(), + scan_interval: 30, + } + } +} + +impl LongRunning for Args { + fn rotate(&self) -> Self { + self.clone() + } +} + +#[derive(Clone, Debug)] +pub struct OperatorProofInputs { + pub proof_bytes: Vec, + pub public_values: Vec, + pub vk_hash: Vec, + pub vk_hash_raw: [u8; 32], + pub zkm_version: String, + pub outputs: OperatorPublicOutputs, +} + +pub struct OperatorWrapperProofBuilder { + client: ProverClient, + proving_key: zkm_sdk::ZKMProvingKey, + verifying_key: zkm_sdk::ZKMVerifyingKey, +} + +impl OperatorWrapperProofBuilder { + pub fn new() -> Self { + let client = ProverClient::new(); + let (proving_key, verifying_key) = client.setup(OPERATOR_WRAPPER); + Self { client, proving_key, verifying_key } + } +} + +pub fn parse_hex_array(value: &str) -> anyhow::Result<[u8; N]> { + let bytes = hex::decode(value.trim_start_matches("0x")) + .with_context(|| format!("failed to decode {N}-byte hex value"))?; + ensure!(bytes.len() == N, "expected {N} bytes, got {}", bytes.len()); + Ok(bytes.try_into().expect("length already checked")) +} + +pub fn read_operator_proof_inputs(path: &str) -> anyhow::Result { + let proof_file = + fs::read(path).with_context(|| format!("failed to read operator proof '{path}'"))?; + let public_values_path = format!("{path}.public_inputs.bin"); + let public_values = fs::read(&public_values_path) + .with_context(|| format!("failed to read operator public inputs '{public_values_path}'"))?; + let vk_hash_path = format!("{path}.vk_hash.bin"); + let vk_hash = fs::read(&vk_hash_path) + .with_context(|| format!("failed to read operator vk hash '{vk_hash_path}'"))?; + let zkm_version_path = format!("{path}.zkm_version.bin"); + let zkm_version = fs::read(&zkm_version_path) + .with_context(|| format!("failed to read operator zkm version '{zkm_version_path}'")) + .and_then(|raw| { + String::from_utf8(raw).with_context(|| format!("invalid UTF-8 in '{zkm_version_path}'")) + })?; + + let proof_bytes = + if let Ok(proof) = bincode::deserialize::(&proof_file) { + ensure!( + proof.public_values.to_vec() == public_values, + "operator proof public values do not match sidecar" + ); + ensure!( + proof.zkm_version == zkm_version, + "operator proof zkm_version does not match sidecar" + ); + proof.bytes() + } else { + proof_file + }; + + let raw_vk_hash = zkm_vk_hash_to_raw(&vk_hash).map_err(anyhow::Error::msg)?; + let outputs = + decode_operator_public_outputs(&public_values, raw_vk_hash).map_err(anyhow::Error::msg)?; + let vk_hash_raw = validate_operator_vk_hash_binding(&outputs, &vk_hash)?; + + Ok(OperatorProofInputs { + proof_bytes, + public_values, + vk_hash, + vk_hash_raw, + zkm_version, + outputs, + }) +} + +pub fn validate_operator_vk_hash_binding( + outputs: &OperatorPublicOutputs, + vk_hash: &[u8], +) -> anyhow::Result<[u8; 32]> { + let raw_vk_hash = zkm_vk_hash_to_raw(vk_hash).map_err(anyhow::Error::msg)?; + ensure!( + outputs.operator_vk_hash == raw_vk_hash, + "operator vk hash does not match authenticated operator public output" + ); + Ok(raw_vk_hash) +} + +impl ProofBuilder for OperatorWrapperProofBuilder { + fn client(&self) -> &zkm_sdk::ProverClient { + &self.client + } + + fn pk(&self) -> &zkm_sdk::ZKMProvingKey { + &self.proving_key + } + + fn vk(&self) -> &zkm_sdk::ZKMVerifyingKey { + &self.verifying_key + } + + fn name() -> String { + "operator-wrapper".to_string() + } + + #[tracing::instrument(level = "info", skip(self, ctx))] + fn build_proof( + &self, + ctx: &ProofRequest, + ) -> anyhow::Result<(Vec, ZKMProofWithPublicValues, u64, f32)> { + let ProofRequest::WrapperProofRequest { + operator_input_proof, + graph_id, + genesis_sequencer_commit_txid, + .. + } = ctx + else { + anyhow::bail!("Invalid proof request type"); + }; + + let operator_inputs = read_operator_proof_inputs(operator_input_proof)?; + let genesis_txid = Txid::from_str(genesis_sequencer_commit_txid) + .with_context(|| format!("invalid genesis txid '{genesis_sequencer_commit_txid}'"))?; + let genesis_txid_bytes = genesis_txid.to_byte_array(); + let expected_constant = hash_operator_constant(*graph_id, genesis_txid_bytes); + ensure!( + operator_inputs.outputs.constant == expected_constant, + "operator public constant does not match wrapper session" + ); + let (proof, cycles, proving_time) = tracing::info_span!("generate wrapper proof") + .in_scope(|| -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { + let mut stdin = ZKMStdin::new(); + stdin.write(&operator_inputs.proof_bytes); + stdin.write(&operator_inputs.public_values); + stdin.write(&operator_inputs.vk_hash); + stdin.write(&operator_inputs.zkm_version); + stdin.write(graph_id); + stdin.write(&genesis_txid_bytes); + + let elf_id = if ELF_ID.get().is_none() { + ELF_ID + .set(hex::encode(Sha256::digest(&self.proving_key.elf))) + .map_err(anyhow::Error::msg)?; + None + } else { + Some(ELF_ID.get().unwrap().clone()) + }; + tracing::info!("elf id: {:?}", elf_id); + + let proving_start = tokio::time::Instant::now(); + let (proof, cycles) = self.client.prove_with_cycles( + &self.proving_key, + &stdin, + ZKMProofKind::Groth16, + elf_id, + )?; + let proving_duration = proving_start.elapsed().as_secs_f32() * 1000.0; + Ok((proof, cycles, proving_duration)) + })?; + + Ok((vec![], proof, cycles, proving_time)) + } + + fn save_proof( + &self, + ctx: &ProofRequest, + _input: &[u8], + _cycles: u64, + proof: ZKMProofWithPublicValues, + ) -> anyhow::Result<(String, usize)> { + let ProofRequest::WrapperProofRequest { output, .. } = ctx else { + anyhow::bail!("invalid context"); + }; + let public_value_hex = hex::encode(proof.public_values.to_vec()); + let serialized_proof = bincode::serialize(&proof)?; + let proof_size = serialized_proof.len(); + let zkm_version = proof.zkm_version.clone(); + fs::write(format!("{output}.public_inputs.bin"), proof.public_values.to_vec())?; + fs::write(format!("{output}.vk_hash.bin"), self.verifying_key.bytes32())?; + fs::write(format!("{output}.zkm_version.bin"), zkm_version)?; + fs::write(output, serialized_proof)?; + Ok((public_value_hex, proof_size)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use bitcoin_light_client_circuit::zkm_vk_hash_from_raw; + + #[derive(serde::Serialize, serde::Deserialize)] + struct LegacyOperatorPublicOutputs { + btc_best_block_hash: [u8; 32], + constant: [u8; 32], + included_watchtowers: [u8; 32], + } + + #[test] + fn decode_operator_public_outputs_accepts_legacy_public_values() { + let actual_vk_hash = [4u8; 32]; + let legacy_outputs = LegacyOperatorPublicOutputs { + btc_best_block_hash: [0u8; 32], + constant: [1u8; 32], + included_watchtowers: [2u8; 32], + }; + let public_values = bincode::serialize(&legacy_outputs).unwrap(); + + let outputs = decode_operator_public_outputs(&public_values, actual_vk_hash).unwrap(); + + assert_eq!(outputs.btc_best_block_hash, legacy_outputs.btc_best_block_hash); + assert_eq!(outputs.constant, legacy_outputs.constant); + assert_eq!(outputs.included_watchtowers, legacy_outputs.included_watchtowers); + assert_eq!(outputs.operator_vk_hash, actual_vk_hash); + } + + #[test] + fn validate_operator_vk_hash_binding_rejects_mismatched_sidecar() { + let outputs = OperatorPublicOutputs { + btc_best_block_hash: [0u8; 32], + constant: [1u8; 32], + included_watchtowers: [2u8; 32], + operator_vk_hash: [3u8; 32], + }; + let mismatched = zkm_vk_hash_from_raw(&[4u8; 32]); + + let err = validate_operator_vk_hash_binding(&outputs, &mismatched).unwrap_err(); + + assert!(err.to_string().contains("operator vk hash does not match")); + } +} diff --git a/circuits/operator-wrapper-proof/host/src/main.rs b/circuits/operator-wrapper-proof/host/src/main.rs new file mode 100644 index 00000000..df1bc2b5 --- /dev/null +++ b/circuits/operator-wrapper-proof/host/src/main.rs @@ -0,0 +1,24 @@ +//! Generate operator wrapper proof. +use clap::Parser; +use operator_wrapper_proof::{Args, OperatorWrapperProofBuilder, parse_hex_array}; +use proof_builder::{ProofBuilder, ProofRequest}; + +#[tokio::main] +async fn main() { + dotenv::dotenv().ok(); + let args = Args::parse(); + zkm_sdk::utils::setup_logger(); + + let builder = OperatorWrapperProofBuilder::new(); + let ctx = ProofRequest::WrapperProofRequest { + operator_proof_id: 0, + operator_input_proof: args.operator_input_proof.clone(), + graph_id: parse_hex_array::<16>(&args.graph_id).unwrap(), + genesis_sequencer_commit_txid: args.genesis_sequencer_commit_txid.clone(), + output: args.output.clone(), + }; + + let (input, proof, cycles, _) = builder.build_proof(&ctx).unwrap(); + tracing::info!("Operator wrapper proof cycles: {cycles}"); + builder.save_proof(&ctx, &input, cycles, proof).unwrap(); +} diff --git a/circuits/proof-builder/Cargo.toml b/circuits/proof-builder/Cargo.toml index 4f630873..cedd6451 100644 --- a/circuits/proof-builder/Cargo.toml +++ b/circuits/proof-builder/Cargo.toml @@ -11,14 +11,14 @@ serde.workspace = true # Ziren zkm-sdk.workspace = true -zkm-prover.workspace = true -zkm-verifier.workspace = true +#zkm-prover.workspace = true +#zkm-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/lib.rs b/circuits/proof-builder/src/lib.rs index 710d9098..9a992e18 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -68,6 +68,13 @@ pub enum ProofRequest { watchtower_challenge_txn_pubkeys: Vec, watchtower_challenge_txn_scripts: Vec, }, + WrapperProofRequest { + operator_proof_id: i64, + operator_input_proof: String, + graph_id: [u8; 16], + genesis_sequencer_commit_txid: String, + output: String, + }, } #[derive(Error, Debug, Clone)] @@ -114,7 +121,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, @@ -133,6 +140,7 @@ pub enum ProofType { StateChain, Operator, Watchtower, + Wrapper, } const HEADER_CHAIN_NAME: &str = "header-chain"; @@ -140,6 +148,7 @@ const COMMIT_CHAIN_NAME: &str = "commit-chain"; const STATE_CHAIN_NAME: &str = "state-chain"; const OPERATOR_NAME: &str = "operator"; const WATCHTOWER_NAME: &str = "watchtower"; +const WRAPPER_NAME: &str = "wrapper"; impl ProofType { pub fn get_chain_name(&self) -> &'static str { match self { @@ -148,6 +157,7 @@ impl ProofType { ProofType::StateChain => STATE_CHAIN_NAME, ProofType::Operator => OPERATOR_NAME, ProofType::Watchtower => WATCHTOWER_NAME, + ProofType::Wrapper => WRAPPER_NAME, } } } @@ -182,6 +192,14 @@ pub struct OperatorProofDescRequest { pub graph_id: String, } +#[derive(Debug, Deserialize)] +pub struct WrapperProofDescRequest { + pub operator_proof_id: Option, + pub instance_id: Option, + pub graph_id: Option, + pub genesis_sequencer_commit_txid: Option, +} + #[derive(Debug, Serialize, Deserialize, Default)] pub struct ProofDescResponse { pub proof_desc: Option, @@ -194,7 +212,7 @@ pub struct OperatorProofRequest { pub graph_id: String, 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 +224,6 @@ pub struct ProofData { pub vk: String, pub public_inputs: Vec, pub zkm_version: String, - pub proof_part_stark_vk: Vec, } impl ProofData { @@ -217,7 +234,8 @@ impl ProofData { | ProofType::CommitChain | ProofType::StateChain | ProofType::Watchtower - | ProofType::Operator => { + | ProofType::Operator + | ProofType::Wrapper => { proof_data.proof = fs::read(path).unwrap_or_default(); proof_data.public_inputs = fs::read(format!("{path}.public_inputs.bin")).unwrap_or_default(); @@ -228,8 +246,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 @@ -242,6 +258,35 @@ pub struct OperatorProofResponse { pub error: Option, } +#[derive(Debug, Serialize, Deserialize, Default)] +pub struct WrapperProofMetadata { + pub id: i64, + pub operator_proof_id: i64, + pub instance_id: String, + pub graph_id: String, + pub operator_path_to_proof: String, + pub path_to_proof: Option, + pub public_value_hex: Option, + pub operator_vk_hash: String, + pub genesis_sequencer_commit_txid: String, + pub operator_public_value_hex: Option, + pub proof_state: i64, + pub proof_size: i64, + pub cycles: i64, + pub total_time_to_proof: i64, + pub proving_time: i64, + pub zkm_version: String, + pub created_at: i64, + pub updated_at: i64, +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct WrapperProofResponse { + pub proof_data: Option, + pub metadata: Option, + pub error: Option, +} + #[derive(Debug, Serialize, Deserialize)] pub struct WatchtowerProofRequest { pub instance_id: String, @@ -298,27 +343,25 @@ mod tests { } #[test] - fn load_proof_data_reads_proof_part_stark_vk_sidecar() { + fn load_proof_data_omits_legacy_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); + let ProofData { proof, vk, public_inputs, zkm_version } = proof_data; - 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]); + assert_eq!(proof, vec![1u8, 2, 3]); + assert_eq!(public_inputs, vec![4u8, 5, 6]); + assert_eq!(vk, "vk-hash"); + assert_eq!(zkm_version, "v1.2.5"); 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/run-operator-wrapper-proof.sh b/circuits/run-operator-wrapper-proof.sh new file mode 100755 index 00000000..f5482db1 --- /dev/null +++ b/circuits/run-operator-wrapper-proof.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +source ~/.zkm-toolchain/env +set -a +source .env +set +a + +require_env() { + local name="$1" + if [ -z "${!name:-}" ]; then + echo "Missing required environment variable: $name" >&2 + exit 1 + fi +} + +require_file() { + local path="$1" + if [ ! -f "$path" ]; then + echo "Missing required file: $path" >&2 + exit 1 + fi +} + +operator_input_proof="${1:-}" +output="${2:-}" +if [ -z "$operator_input_proof" ] || [ -z "$output" ]; then + echo "Usage: bash run-operator-wrapper-proof.sh " >&2 + exit 1 +fi + +require_env "GRAPH_ID" +require_env "GENESIS_SEQUENCER_COMMIT_TXID" + +require_file "$operator_input_proof" +require_file "${operator_input_proof}.public_inputs.bin" +require_file "${operator_input_proof}.vk_hash.bin" +require_file "${operator_input_proof}.zkm_version.bin" + +mkdir -p "$(dirname "$output")" + +graph_id_hex="${GRAPH_ID//-/}" + +cargo build -r --bin operator-wrapper-proof --package operator-wrapper-proof +CMD="../target/release/operator-wrapper-proof" + +RUST_LOG=info "$CMD" \ + --operator-input-proof "$operator_input_proof" \ + --graph-id "$graph_id_hex" \ + --genesis-sequencer-commit-txid "$GENESIS_SEQUENCER_COMMIT_TXID" \ + --output "$output" diff --git a/circuits/state-chain-proof/guest/Cargo.lock b/circuits/state-chain-proof/guest/Cargo.lock index 612d3e5b..22665830 100644 --- a/circuits/state-chain-proof/guest/Cargo.lock +++ b/circuits/state-chain-proof/guest/Cargo.lock @@ -2225,7 +2225,6 @@ dependencies = [ "sha2 0.10.9", "state-chain", "tracing", - "zkm-verifier", "zkm-zkvm", ] @@ -2376,14 +2375,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -5114,7 +5113,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -5135,7 +5134,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -5704,6 +5703,13 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/circuits/state-chain-proof/guest/Cargo.toml b/circuits/state-chain-proof/guest/Cargo.toml index 9e195a81..66d0f092 100644 --- a/circuits/state-chain-proof/guest/Cargo.toml +++ b/circuits/state-chain-proof/guest/Cargo.toml @@ -12,8 +12,6 @@ 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 = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging diff --git a/circuits/watchtower-proof/guest/Cargo.lock b/circuits/watchtower-proof/guest/Cargo.lock index af3cab9f..0dd35c97 100644 --- a/circuits/watchtower-proof/guest/Cargo.lock +++ b/circuits/watchtower-proof/guest/Cargo.lock @@ -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", @@ -1282,7 +1282,7 @@ checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "commit-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-primitives", "base64 0.21.7", @@ -1297,7 +1297,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -2276,7 +2276,6 @@ dependencies = [ "sha2 0.10.9", "state-chain", "tracing", - "zkm-verifier", "zkm-zkvm", ] @@ -2427,14 +2426,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" [[package]] name = "header-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -5165,7 +5164,7 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "state-chain" -version = "0.3.3" +version = "0.4.0" dependencies = [ "alloy-consensus", "alloy-primitives", @@ -5186,7 +5185,7 @@ dependencies = [ "tendermint", "tendermint-light-client-verifier", "tracing", - "zkm-verifier", + "verifier", "zkm-zkvm", ] @@ -5755,6 +5754,13 @@ dependencies = [ "serde", ] +[[package]] +name = "verifier" +version = "0.4.0" +dependencies = [ + "zkm-verifier", +] + [[package]] name = "version_check" version = "0.9.5" diff --git a/circuits/watchtower-proof/guest/Cargo.toml b/circuits/watchtower-proof/guest/Cargo.toml index fca4952e..d3a665fe 100644 --- a/circuits/watchtower-proof/guest/Cargo.toml +++ b/circuits/watchtower-proof/guest/Cargo.toml @@ -15,8 +15,6 @@ bitcoin-light-client-circuit = { path = "../../../crates/bitcoin-light-client-ci # 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 = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } # Statically turns off logging diff --git a/circuits/watchtower-proof/guest/src/main.rs b/circuits/watchtower-proof/guest/src/main.rs index c522794c..2a5431b7 100644 --- a/circuits/watchtower-proof/guest/src/main.rs +++ b/circuits/watchtower-proof/guest/src/main.rs @@ -2,12 +2,8 @@ #![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() { @@ -16,17 +12,16 @@ pub fn main() { 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/src/lib.rs b/circuits/watchtower-proof/host/src/lib.rs index d01589a9..d7aa2ccc 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,10 +20,7 @@ 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)] @@ -104,33 +95,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 @@ -249,22 +213,6 @@ impl ProofBuilder for WatchtowerProofBuilder { 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)?; @@ -303,7 +251,6 @@ impl ProofBuilder for WatchtowerProofBuilder { 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 @@ -344,65 +291,9 @@ impl ProofBuilder for WatchtowerProofBuilder { 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)?; Ok((public_value_hex, proof_size)) } } - -#[cfg(test)] -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()), - ) - .unwrap(); - - assert_eq!(requested, vec![new_vk.clone(), old_vk.clone(), new_vk, old_vk]); - } -} diff --git a/crates/bitcoin-light-client-circuit/Cargo.toml b/crates/bitcoin-light-client-circuit/Cargo.toml index 172e5530..b651533d 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,13 @@ 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", ] } [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 dd4b166b..e69de29b 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 bd3560d3..922500e4 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -1,38 +1,36 @@ -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, + CommitChainCircuitInput, CommitChainPrevProofType, decode_commit_chain_circuit_output, + extract_data_from_commitment_outputs, parse_commit_chain_commitment, sequencer_hash, }; use header_chain::{ BitcoinMerkleTree, CircuitBlockHeader, CircuitTransaction, HeaderChainCircuitInput, HeaderChainPrevProofType, MMRHost, SPV, verify_merkle_proof, }; use state_chain::{StateChainCircuitInput, StateChainPrevProofType}; -use std::panic::{AssertUnwindSafe, catch_unwind}; use zkm_primitives::io::ZKMPublicValues; -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; -use bitcoin::{ScriptBuf, TxOut, Txid, secp256k1::PublicKey}; +use bitcoin::{ + ScriptBuf, Transaction, TxOut, Txid, + secp256k1::{PublicKey, 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; @@ -49,6 +47,32 @@ pub struct OperatorPublicOutputs { pub btc_best_block_hash: [u8; 32], pub constant: [u8; 32], pub included_watchtowers: [u8; 32], + pub operator_vk_hash: [u8; 32], +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +struct LegacyOperatorPublicOutputs { + btc_best_block_hash: [u8; 32], + constant: [u8; 32], + included_watchtowers: [u8; 32], +} + +pub fn decode_operator_public_outputs( + public_values: &[u8], + actual_operator_vk_hash: [u8; 32], +) -> Result { + if let Ok(outputs) = bincode::deserialize::(public_values) { + return Ok(outputs); + } + + let legacy_outputs = bincode::deserialize::(public_values) + .map_err(|err| format!("failed to decode operator public values: {err}"))?; + Ok(OperatorPublicOutputs { + btc_best_block_hash: legacy_outputs.btc_best_block_hash, + constant: legacy_outputs.constant, + included_watchtowers: legacy_outputs.included_watchtowers, + operator_vk_hash: actual_operator_vk_hash, + }) } pub fn watch_longest_chain( @@ -57,21 +81,26 @@ pub fn watch_longest_chain( 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 + 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 prev_output = decode_commit_chain_circuit_output(&commit_chain.zkm_public_values); + let prev_proof = CommitChainPrevProofType::PrevProof(prev_output); + let CommitChainPrevProofType::PrevProof(commit_chain_output) = &prev_proof else { + panic!("Only PrevProof is supported in watch_longest_chain"); + }; assert_eq!( commit_chain_output.chain_state.commit_txn.compute_txid(), @@ -81,16 +110,11 @@ pub fn watch_longest_chain( 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( + 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"); @@ -103,16 +127,11 @@ pub fn watch_longest_chain( println!("SPV"); assert!(spv.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); - 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( + 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(); @@ -120,17 +139,6 @@ pub fn watch_longest_chain( 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"); - // check the signature. let cosmos_block_bytes = &state_chain_output.chain_state.latest_cosmos_block; let cosmos_block: LightBlock = @@ -145,19 +153,20 @@ pub fn watch_longest_chain( // check commit chain's genesis block let commitment = commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); + let commitment = parse_commit_chain_commitment(&commitment); if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment[0..32], x); + assert_eq!(commitment.sequencer_set_hash, x); } else { panic!("Invalid commitment: inconsistent sequencer set hash"); }; - assert_eq!(commitment[32..64], state_chain_output.chain_state.genesis_evm_block_hash[..]); + assert_eq!( + commitment.genesis_evm_block_hash, + 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, commit_chain_output.chain_state.block_height) } pub fn u256_to_le_bits(u: U256) -> [bool; 256] { @@ -178,6 +187,27 @@ pub fn le_bits_to_u256(bits: &[bool]) -> U256 { u } +pub fn verify_fixed_watchtower_pubkey( + fixed_watchtower_xonly_public_keys: &[[u8; 32]], + index: usize, + pubkey: &PublicKey, +) -> Result<(), String> { + // The fixed list is order-sensitive: index must match graph watchtower_pubkeys/node_index. + let Some(expected) = fixed_watchtower_xonly_public_keys.get(index) else { + return Err(format!("watchtower index {index} exceeds fixed watchtower list")); + }; + let xonly: XOnlyPublicKey = (*pubkey).into(); + let actual = xonly.serialize(); + if &actual != expected { + return Err(format!( + "watchtower[{index}] pubkey mismatch: actual={}, expected={}", + hex::encode(actual), + hex::encode(expected) + )); + } + Ok(()) +} + // 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( @@ -189,28 +219,29 @@ pub fn propose_longest_chain( watchtower_challenge_txn_pubkey: Vec, watchtower_challenge_txn_scripts: Vec, watchtower_challenge_txn_prev_outs: Vec, + fixed_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 { + actual_operator_vk_hash: [u8; 32], +) -> ([u8; 32], [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, + 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 prev_output = decode_commit_chain_circuit_output(&commit_chain.zkm_public_values); + let prev_proof = CommitChainPrevProofType::PrevProof(prev_output); + let CommitChainPrevProofType::PrevProof(commit_chain_output) = &prev_proof else { + panic!("Only PrevProof is supported in propose_longest_chain"); + }; assert_eq!( commit_chain_output.chain_state.commit_txn.compute_txid(), spv_ss_commit.transaction.0.compute_txid() @@ -222,16 +253,11 @@ 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( + 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(); @@ -239,11 +265,6 @@ pub fn propose_longest_chain( 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 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 @@ -253,55 +274,133 @@ pub fn propose_longest_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:?}"); + let included_watchtowers_bits = u256_to_le_bits(included_watchtowers); + println!("included watchtowers:{included_watchtowers_bits:?}"); + + let mut valid_included_watchtower_count = 0usize; // 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] { + if included_watchtowers_bits[i] { let tx = &watchtower_challenge_txns[i]; + println!("Verify watchtower[{i}] tx: {}, {:?}", tx.compute_txid(), tx); let prev_out = &watchtower_challenge_txn_prev_outs[i]; + let prev_index = tx.input[0].previous_output.vout as usize; let pubkey = &watchtower_challenge_txn_pubkey[i]; - let watchtower_outputs = verify_included_watchtower_challenge( - i, - &graph_id, + + if let Err(err) = + verify_fixed_watchtower_pubkey(fixed_watchtower_xonly_public_keys, i, pubkey) + { + println!("Watchtower[{i}] fixed pubkey verification: {err}"); + continue; + } + + let sig = match tx + .input + .first() + .and_then(|input| input.witness.iter().next()) + .map(bitcoin::taproot::Signature::from_slice) + { + Some(Ok(sig)) => sig, + Some(Err(err)) => { + println!("Watchtower[{i}] invalid taproot signature: {err}"); + continue; + } + None => { + println!("Watchtower[{i}] missing taproot signature"); + continue; + } + }; + // check tx signature is valid + match verify_taproot_leaf_schnorr_signature( + &watchtower_challenge_txn_scripts[i], tx, + prev_index, 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}")); + &sig, + ) { + Ok(_) => {} + Err(msg) => { + println!("Watchtower[{i}] signature verification: {msg}"); + continue; + } + }; + + 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, + watchtower_total_work, + watchtower_consensus_block_height, + zkm_version, + ) = match parse_watchtower_commitment(commitment) { + Ok(c) => c, + Err(err) => { + println!("Watchtower[{i}] parse commitment error, {err}"); + continue; + } + }; + + match verify_groth16_proof(&proof, &public_values, &vk, &zkm_version) { + Ok(_) => {} + Err(err) => { + println!("Watchtower[{i}] invalid proof: {err}"); + continue; + } + } - println!( - "watchtower total work: {:?}", - U256::from_be_bytes(watchtower_outputs.total_work) - ); + if parsed_graph_id != graph_id { + println!( + "Watchtower[{i}] invalid commitment: graph id: parsed = {}, expected = {}", + hex::encode(parsed_graph_id), + hex::encode(graph_id) + ); + continue; + } + println!("check total work with watchtower {i}"); + + // extract ChainState + // check watchtower_chain_state.total_work <= operator_header_chain.total_work + println!("watchtower total work: {:?}", U256::from_be_bytes(watchtower_total_work)); println!("operator total work: {operator_total_work:?}"); + println!( "watchtower_consensus_block_height : {:?}", - U32::from_le_bytes(watchtower_outputs.consensus_block_height) + U32::from_le_bytes(watchtower_consensus_block_height) ); println!("operator_consensus_block_height : {operator_consensus_block_height:?}"); + + if U256::from_be_bytes(watchtower_total_work) > U256::from_be_bytes(operator_total_work) + { + println!("Watchtower[{i}] total work exceeds operator total work"); + continue; + } + // check watchtower.consensus.block_height <= consensus.block_height + if U32::from_le_bytes(watchtower_consensus_block_height) + > operator_consensus_block_height + { + println!("Watchtower[{i}] consensus block height exceeds operator block height"); + continue; + } + + valid_included_watchtower_count += 1; } } + assert!(valid_included_watchtower_count > 0, "no included watchtower passed verification"); 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( + 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"); @@ -310,13 +409,6 @@ pub fn propose_longest_chain( 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"); - // check the signature. let cosmos_block_bytes = &state_chain_output.chain_state.latest_cosmos_block; let cosmos_block: LightBlock = @@ -329,12 +421,16 @@ pub fn propose_longest_chain( // check commit chain's genesis block let commitment = commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); + let commitment = parse_commit_chain_commitment(&commitment); if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment[0..32], x); + assert_eq!(commitment.sequencer_set_hash, x); } else { panic!("Invalid commitment: inconsistent sequencer set hash"); }; - assert_eq!(commitment[32..64], state_chain_output.chain_state.genesis_evm_block_hash[..]); + assert_eq!( + commitment.genesis_evm_block_hash, + state_chain_output.chain_state.genesis_evm_block_hash + ); assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); @@ -370,10 +466,25 @@ pub fn propose_longest_chain( ); //operator_public_input - OperatorPublicOutputs { - btc_best_block_hash: operator_committed_blockhash, + ( + operator_committed_blockhash, constant, - included_watchtowers: included_watchtowers.to_le_bytes::<32>(), + included_watchtowers.to_le_bytes::<32>(), + resolve_operator_vk_hash( + commit_chain_output.chain_state.operator_vk_hash, + actual_operator_vk_hash, + ), + ) +} + +pub fn resolve_operator_vk_hash( + commit_chain_operator_vk_hash: [u8; 32], + actual_operator_vk_hash: [u8; 32], +) -> [u8; 32] { + if commit_chain_operator_vk_hash == commit_chain::LEGACY_OPERATOR_VK_HASH { + actual_operator_vk_hash + } else { + commit_chain_operator_vk_hash } } @@ -388,6 +499,51 @@ pub fn hash_operator_constant( *hash.as_byte_array() } +pub const WRAPPER_PUBLIC_VALUES_SIZE: usize = 32 + GRAPH_ID_SIZE + 32; + +pub fn wrapper_public_values( + operator_vk_hash: [u8; 32], + graph_id: [u8; GRAPH_ID_SIZE], + genesis_sequencer_commit_txid: [u8; 32], +) -> [u8; WRAPPER_PUBLIC_VALUES_SIZE] { + let mut public_values = [0u8; WRAPPER_PUBLIC_VALUES_SIZE]; + public_values[..32].copy_from_slice(&operator_vk_hash); + public_values[32..32 + GRAPH_ID_SIZE].copy_from_slice(&graph_id); + public_values[32 + GRAPH_ID_SIZE..].copy_from_slice(&genesis_sequencer_commit_txid); + public_values +} + +// zkm_vk_hash: 66 bytes, prefix with '0x' +pub fn zkm_vk_hash_to_raw(vk_hash: &[u8]) -> Result<[u8; 32], String> { + let vk_hash = + std::str::from_utf8(vk_hash).map_err(|err| format!("invalid zkm vk hash UTF-8: {err}"))?; + let Some(hex_hash) = vk_hash.strip_prefix("0x") else { + return Err("zkm vk hash must have 0x prefix".to_string()); + }; + if hex_hash.len() != 64 { + return Err(format!("zkm vk hash must be 64 hex chars, got {}", hex_hash.len())); + } + let bytes = hex::decode(hex_hash).map_err(|err| format!("invalid zkm vk hash hex: {err}"))?; + bytes.try_into().map_err(|_| "zkm vk hash must decode to 32 bytes".to_string()) +} + +pub fn zkm_vk_hash_from_raw(raw: &[u8; 32]) -> Vec { + format!("0x{}", hex::encode(raw)).into_bytes() +} + +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() +} + /// Utility method for converting u32 words to bytes in big endian. pub fn words_to_bytes_be(words: &[u32; 8]) -> [u8; 32] { let mut bytes = [0u8; 32]; @@ -452,54 +608,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 +650,67 @@ 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)) -} - -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()) -} + end += ZKM_VERSION_LEN_SIZE; -/// 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(); + let zkm_version = String::from_utf8(commitment[end..end + zkm_version_len].to_vec()) + .map_err(|err| format!("invalid zkm_version UTF-8: {err}"))?; - Ok(output) -} - -/// 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] = @@ -720,43 +718,21 @@ mod tests { const VK_HASH: &str = include_str!("../../../circuits/data/watchtower/output3.bin.vk_hash.bin"); 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")); - } - } - } - #[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 +741,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 +759,25 @@ 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_u256_to_le_bits() { use std::str::FromStr; @@ -817,216 +812,56 @@ mod tests { } #[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())]; + fn test_zkm_vk_hash_to_raw_accepts_prefixed_hash() { + let raw = [0xabu8; 32]; + let prefixed = zkm_vk_hash_from_raw(&raw); - assert_eq!( - attested_part_stark_vk_for_zkm_version(&unique_witnesses, ZKM_VERSION).unwrap(), - part_stark_vk - ); + assert_eq!(zkm_vk_hash_to_raw(&prefixed).unwrap(), raw); } #[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(); + fn test_zkm_vk_hash_to_raw_rejects_unprefixed_hash() { + let raw = [0xcdu8; 32]; + let unprefixed = hex::encode(raw); - assert_eq!(parsed, expected); + assert!(zkm_vk_hash_to_raw(unprefixed.as_bytes()).is_err()); } } diff --git a/crates/bitcoin-light-client-circuit/src/utils.rs b/crates/bitcoin-light-client-circuit/src/utils.rs index 02ee439c..e796e6ed 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; 96], 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 72% rename from crates/bitvm2-ga/Cargo.toml rename to crates/bitvm-gc/Cargo.toml index 4210348b..f84c8d57 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,9 @@ clap = { workspace = true, features = ["derive"] } strum = { workspace = true, features = ["derive"] } [dev-dependencies] -client = { workspace = true } -esplora-client = { workspace = true } -tokio = { workspace = true, features = ["full"] } -bitcoincore-rpc = "0.19" -bitcoin-old = { version = "0.31.2", package = "bitcoin" } +ark-crypto-primitives = { workspace = true } +rand_chacha = { workspace = true } [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 7f664bb3..747e7d7b 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 00000000..ccce0658 --- /dev/null +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -0,0 +1,1083 @@ +use rayon::prelude::*; +use std::collections::HashSet; +use std::panic::{AssertUnwindSafe, catch_unwind}; +use std::path::PathBuf; + +use anyhow::{Result, bail}; +use ark_bn254::{Bn254, Fq, Fr}; +use ark_groth16::VerifyingKey as Groth16VerifyingKey; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use garbled_snark_verifier::bag::S; +use goat::assert_scripts::{ + INPUT_WIRE_NUM, OperatorAssertPublicKey, 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::{ + LAMPORT_N, ProverSetupState, WeKnownPi1SetupCt as RealSetupCt, babe_verifier_presign, +}; +use verifiable_circuit_babe::cac::{ + CACSetupPackage as RealCACSetupPackage, FinalizedInstanceData as RealFinalizedInstanceData, + cac_finalize_indices, +}; +use verifiable_circuit_babe::gc::{ + SparseAdaptorEntry as RealSparseAdaptorEntry, SparseAdaptorRow as RealSparseAdaptorRow, + SparseAdaptorTable as RealSparseAdaptorTable, +}; +use verifiable_circuit_babe::instance::BABEInstance; +use verifiable_circuit_babe::instance::commit::CACInstanceCommit as RealCACInstanceCommit; +use verifiable_circuit_babe::prover::BABEProver; +use verifiable_circuit_babe::soldering::{ + SolderingData as RealSolderingData, SolderingProof as RealSolderingProof, +}; +use verifiable_circuit_babe::verifier::BABEVerifier; + +use crate::types::BitvmGcCircuitData; + +/// 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; +// TODO: use verifiable_circuit_babe::babe::M_CC instead +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 CACSetupPackage { + pub commits: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CACInstanceCommit { + pub epk: Vec<[[u8; 20]; 2]>, + pub wots_padding_epk: [[[u8; 20]; 2]; 4], + pub constant_commits: [[[u8; 32]; 2]; 2], + pub h_msg: [u8; 20], + pub h_ct_setup: [u8; 32], + pub com_adaptor: [u8; 32], + pub com_gc: [u8; 32], +} + +#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct FinalizedInstanceData { + pub index: usize, + pub final_msg_hash: [u8; 20], + pub wire_hashes: Vec, + pub gc_commitment: [u8; 32], + pub adaptor_commitment: [u8; 32], + pub ct_setup_commitment: [u8; 32], + pub real_data: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RealFinalizedPayload { + pub gc_ciphertexts: Vec>, + pub adaptor_table: SerializableSparseAdaptorTable, + pub ct_setup: SerializableSetupCt, + pub constant_labels: [[u8; 16]; 2], + pub wots_padding_zero_labels: [[u8; 16]; 4], +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct CompactFinalizedInstanceData { + pub index: usize, + pub real_data: RealFinalizedPayload, +} + +#[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 SerializableSetupCt { + pub ct2_r_delta_g2: Vec, + pub ct3_masked_msg: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableSparseAdaptorTable { + pub entries: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableSparseAdaptorEntry { + pub x: SerializableSparseAdaptorRow, + pub y: SerializableSparseAdaptorRow, + pub z: SerializableSparseAdaptorRow, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SerializableSparseAdaptorRow { + pub cts: Vec<[u8; 32]>, + pub offset: Vec, +} + +#[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, Default)] +pub struct SolderedLabelsData { + pub base_commitment: Vec<([u8; 32], [u8; 32])>, + pub deltas: Vec>, + pub commitments: Vec>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeVerifierPrivateState { + pub instance_seeds: Vec, + pub temp_val: [u8; 32], + 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 BabeAssertWitness { + pub pi1: Vec, + #[serde(default)] + pub pubin_commitment: [u8; 32], + pub wots_sig: Vec<[u8; 21]>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeChallengeAssertWitness { + pub verifier_index: usize, + pub input_labels: Vec<[u8; 16]>, + pub wots_sig: Vec<[u8; 21]>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct BabeWronglyChallengedWitness { + pub verifier_index: usize, + pub final_msgs: Vec>, +} + +impl CACInstanceCommit { + /// Builds deterministic placeholder setup commitments for tests and wiring. + pub fn sample(seed: u8) -> Self { + let epk = (0..LAMPORT_N) + .map(|wire| [hash20(&[seed, wire as u8, 0]), hash20(&[seed, wire as u8, 1])]) + .collect(); + Self { + epk, + wots_padding_epk: padding_wire_hashes(), + constant_commits: [ + [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])], + [hash32(&[seed, 0xf1, 0]), hash32(&[seed, 0xf1, 1])], + ], + h_msg: hash20(&[seed, 0xa0]), + h_ct_setup: hash32(&[seed, 0xa1]), + com_adaptor: hash32(&[seed, 0xa2]), + com_gc: hash32(&[seed, 0xa3]), + } + } +} + +impl FinalizedInstanceData { + /// Builds deterministic placeholder finalized data for tests and graph wiring. + pub fn sample(index: usize) -> Self { + let seed = index as u8; + let wire_hashes = (0..INPUT_WIRE_NUM) + .map(|wire| WireHash { + true_label_hash: hash20(&[seed, wire as u8, 1]), + false_label_hash: hash20(&[seed, wire as u8, 0]), + }) + .collect(); + Self { + index, + final_msg_hash: hash20(&[seed, 0xb0]), + wire_hashes, + gc_commitment: hash32(&[seed, 0xb1]), + adaptor_commitment: hash32(&[seed, 0xb2]), + ct_setup_commitment: hash32(&[seed, 0xb3]), + real_data: None, + } + } +} + +impl CompactFinalizedInstanceData { + pub fn try_from_finalized(finalized: &FinalizedInstanceData) -> Result { + let real_data = finalized.real_data.clone().ok_or_else(|| { + anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) + })?; + Ok(Self { index: finalized.index, real_data }) + } +} + +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| CACInstanceCommit::sample(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, + public_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, public_inputs))) + .map_err(|_| anyhow::anyhow!("real BABE verifier setup panicked while loading GC assets"))? + .map_err(anyhow::Error::msg)?; + let package = from_real_package(&verifier.commit()); + let private_state = BabeVerifierPrivateState { + instance_seeds: verifier.instances.iter().map(|instance| instance.seed).collect(), + temp_val: verifier.temp_val, + statement_digest: statement_digest(vk, public_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, + public_inputs: &[Fr], +) -> Result { + ensure_real_gc_assets_configured()?; + if private_state.statement_digest != statement_digest(vk, public_inputs)? { + bail!("BABE setup statement does not match persisted verifier state"); + } + validate_finalized_indices(package, finalized_indices)?; + let verifier = restore_real_verifier(private_state, vk, public_inputs)?; + if from_real_package(&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 + .iter() + .map(|data| from_real_finalized(data, package)) + .collect::>>()?, + from_real_soldering(&bundle.soldering)?, + )) +} + +/// Verifies real CAC openings, commitments, and the native Ziren soldering proof. +pub fn verify_real_setup( + soldering_builder: &BabeBundleBuilder, + package: &CACSetupPackage, + opened: &[(usize, u64)], + finalized: &[FinalizedInstanceData], + soldering: &SolderingData, + vk: &Groth16VerifyingKey, + public_inputs: &[Fr], +) -> Result<()> { + let real_package = to_real_package(package); + let real_finalized = finalized + .iter() + .map(to_real_finalized) + .collect::>>()?; + + let bundle = BabeBundle { + opened: opened.to_vec(), + finalized: real_finalized, + soldering: to_real_soldering(soldering)?, + temp_hashlock: [0u8; 20], + }; + soldering_builder + .babe_prover_verify_setup(&real_package, &bundle, vk, public_inputs) + .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 + .iter() + .map(CompactFinalizedInstanceData::try_from_finalized) + .collect::>>()?, + 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( + package: &CACSetupPackage, + payload: CompactSolderingProofPayload, +) -> Result { + let finalized = payload + .finalized + .into_iter() + .map(|data| expand_compact_finalized_instance(package, data)) + .collect::>>()?; + 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(&to_real_package(package), 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| FinalizedInstanceData::sample(*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); + } + if data.wire_hashes.len() != INPUT_WIRE_NUM { + bail!("finalized index {} has invalid wire hash count", data.index); + } + } + Ok(()) +} + +/// Extracts one graph slot owned by `verifier_pubkey` from finalized setup data. +pub fn extract_gc_circuit_data( + finalized: &[FinalizedInstanceData], + soldering: &SolderingData, + verifier_pubkey: bitcoin::PublicKey, +) -> Result { + if finalized.len() != BABE_M_CC { + bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); + } + if soldering.finalized_indices != finalized.iter().map(|data| data.index).collect::>() { + bail!("soldering finalized indices mismatch"); + } + let data = &finalized[0]; + let wire_hashes: [WireHash; INPUT_WIRE_NUM] = + data.wire_hashes.clone().try_into().map_err(|wire_hashes: Vec| { + anyhow::anyhow!( + "BABE input label count {} is incompatible with GOAT verifier connector wire count {INPUT_WIRE_NUM}", + wire_hashes.len() + ) + })?; + Ok(BitvmGcCircuitData { + verifier_pubkey, + final_msg_hashlocks: finalized.iter().map(|data| data.final_msg_hash).collect(), + wire_hashes, + }) +} + +/// Builds the native BABE assertion witness from the validated wrapper Groth16 proof. +pub fn build_assert_witness( + proof: &ark_groth16::Proof, + assert_secret_key: &OperatorAssertSecretKey, +) -> Result { + build_assert_witness_with_pubin_commitment(proof, &[0u8; 32], assert_secret_key) +} + +pub fn build_assert_witness_with_pubin_commitment( + proof: &ark_groth16::Proof, + pubin_commitment: &[u8; 32], + assert_secret_key: &OperatorAssertSecretKey, +) -> Result { + if assert_secret_key.is_empty() { + bail!("operator WOTS secret key must not be empty"); + } + let pi1 = proof.a; + let mut pi1_bytes = Vec::new(); + pi1.serialize_compressed(&mut pi1_bytes).expect("serialize pi1"); + let msg = pi1_to_wots96_msg(&pi1, pubin_commitment); + let wots_sig = Wots96::sign(assert_secret_key, &msg); + Ok(BabeAssertWitness { + pi1: pi1_bytes, + pubin_commitment: *pubin_commitment, + wots_sig: wots_sig.to_vec(), + }) +} + +pub fn assert_wots_message(assert_witness: &BabeAssertWitness) -> Result<[u8; 96]> { + let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) + .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; + Ok(pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment)) +} + +/// Builds a placeholder verifier challenge witness from an assert witness. +pub fn build_challenge_assert_witness( + verifier_state: &BabeVerifierState, + assert_witness: &BabeAssertWitness, + 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"); + } + if assert_witness.pi1.is_empty() || assert_witness.wots_sig.is_empty() { + bail!("invalid assert witness"); + } + Ok(BabeChallengeAssertWitness { + verifier_index, + input_labels: (0usize..INPUT_WIRE_NUM) + .map(|index| hash16_with_index(&assert_witness.pi1, index)) + .collect(), + wots_sig: assert_witness.wots_sig.clone(), + }) +} + +/// Verifies a native operator assertion and reveals the real base-instance labels. +#[allow(clippy::too_many_arguments)] +pub fn build_real_challenge_assert_witness( + private_state: &BabeVerifierPrivateState, + package: &CACSetupPackage, + finalized_indices: &[usize], + vk: &Groth16VerifyingKey, + public_inputs: &[Fr], + operator_wots_pubkey: &OperatorAssertPublicKey, + assert_witness: &BabeAssertWitness, + 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, vk, public_inputs)?; + if from_real_package(&verifier.commit()) != *package { + bail!("persisted BABE verifier state does not reproduce setup package"); + } + let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) + .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; + let wots_sig = to_real_wots_sig(&assert_witness.wots_sig)?; + let signed_message = Wots96::signature_to_message(&wots_sig); + let expected_message = pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment); + if signed_message != expected_message { + bail!("operator BABE assertion WOTS signature message does not match pi1/pubin"); + } + let base_idx = finalized_indices[0]; + let base_inst = verifier + .instances + .get(base_idx) + .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; + let mut input_labels = base_inst + .compute_pi1_labels_based_on_value(pi1) + .into_iter() + .skip(2) + .map(|label| label.0) + .collect::>(); + let commit = package + .commits + .get(base_idx) + .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; + input_labels.extend(pubin_input_labels(commit, &assert_witness.pubin_commitment)); + if input_labels.len() != INPUT_WIRE_NUM { + bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", input_labels.len()); + } + let _ = operator_wots_pubkey; + Ok(BabeChallengeAssertWitness { verifier_index, input_labels, wots_sig: wots_sig.to_vec() }) +} + +/// Builds a wrongly-challenged witness from every recovered finalized-message preimage. +pub fn build_wrongly_challenged_witness( + prover_state: &BabeProverState, + challenge_witness: &BabeChallengeAssertWitness, + final_msgs: Vec>, +) -> Result { + build_wrongly_challenged_witness_from_preimages( + &prover_state.h_msgs, + challenge_witness, + final_msgs, + ) +} + +/// Evaluates the native BABE garbled circuit and returns all finalized hashlock preimages. +pub fn recover_real_wrongly_challenged_witness( + prover_state: &BabeProverState, + challenge_witness: &BabeChallengeAssertWitness, + proof: &ark_groth16::Proof, +) -> Result { + let real_state = ProverSetupState { + wots_sk_p: vec![], + finalized: prover_state + .finalized + .iter() + .map(to_real_finalized) + .collect::>>()?, + soldering: to_real_soldering(&prover_state.soldering)?, + h_msgs: prover_state.h_msgs.clone(), + presigs_v: babe_verifier_presign(), + }; + recover_all_finalized_messages(prover_state, challenge_witness, proof, &real_state) +} + +/// 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_msgs: Vec>, +) -> Result { + if h_msgs.len() != BABE_M_CC { + bail!("wrongly challenged setup must contain exactly {BABE_M_CC} finalized hashlocks"); + } + if final_msgs.len() != h_msgs.len() { + bail!("wrongly challenged witness must provide every finalized preimage"); + } + for (position, (final_msg, expected_hash)) in final_msgs.iter().zip(h_msgs).enumerate() { + if label_hash(final_msg) != *expected_hash { + bail!("message at finalized position {position} is not a valid preimage"); + } + } + Ok(BabeWronglyChallengedWitness { + verifier_index: challenge_witness.verifier_index, + final_msgs, + }) +} + +fn ensure_real_gc_assets_configured() -> Result<()> { + for name in ["GC_GATES_PATH", "GC_INDICES_PATH"] { + let path = PathBuf::from( + std::env::var(name) + .map_err(|_| anyhow::anyhow!("{name} is required for real BABE setup"))?, + ); + if !path.is_file() { + bail!("{name} does not point to a readable file: {}", path.display()); + } + } + Ok(()) +} + +fn statement_digest(vk: &Groth16VerifyingKey, public_inputs: &[Fr]) -> Result<[u8; 32]> { + let mut bytes = Vec::new(); + vk.serialize_compressed(&mut bytes)?; + for public_input in public_inputs { + public_input.serialize_compressed(&mut bytes)?; + } + Ok(hash32(&bytes)) +} + +fn restore_real_verifier( + state: &BabeVerifierPrivateState, + vk: &Groth16VerifyingKey, + public_inputs: &[Fr], +) -> Result { + let instances = state + .instance_seeds + .par_iter() + .map(|seed| { + let mut instance = BABEInstance::new_from_seed(*seed); + instance.enc_setup(vk, public_inputs).map_err(anyhow::Error::msg)?; + Ok(instance) + }) + .collect::>>()?; + Ok(BABEVerifier { instances, temp_val: state.temp_val }) +} + +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_package(package: &RealCACSetupPackage) -> CACSetupPackage { + CACSetupPackage { commits: package.commits.iter().map(from_real_commit).collect() } +} + +fn from_real_commit(commit: &RealCACInstanceCommit) -> CACInstanceCommit { + CACInstanceCommit { + epk: commit.epk.clone(), + wots_padding_epk: padding_wire_hashes(), + constant_commits: commit.constant_commits, + h_msg: commit.h_msg, + h_ct_setup: commit.h_ct_setup, + com_adaptor: commit.com_adaptor, + com_gc: commit.com_gc, + } +} + +fn to_real_package(package: &CACSetupPackage) -> RealCACSetupPackage { + RealCACSetupPackage { + commits: package + .commits + .iter() + .map(|commit| RealCACInstanceCommit { + epk: commit.epk.clone(), + constant_commits: commit.constant_commits, + h_msg: commit.h_msg, + h_ct_setup: commit.h_ct_setup, + com_adaptor: commit.com_adaptor, + com_gc: commit.com_gc, + }) + .collect(), + } +} + +fn from_real_finalized( + finalized: &RealFinalizedInstanceData, + package: &CACSetupPackage, +) -> Result { + let real_data = RealFinalizedPayload { + gc_ciphertexts: finalized + .gc_ciphertexts + .iter() + .map(|ciphertext| ciphertext.map(|label| label.0)) + .collect(), + adaptor_table: from_real_adaptor_table(&finalized.adaptor_table), + ct_setup: SerializableSetupCt { + ct2_r_delta_g2: finalized.ct_setup.ct2_r_delta_g2.clone(), + ct3_masked_msg: finalized.ct_setup.ct3_masked_msg.clone(), + }, + constant_labels: [finalized.constant_labels[0].0, finalized.constant_labels[1].0], + wots_padding_zero_labels: [[0u8; 16]; 4], + }; + expand_compact_finalized_instance( + package, + CompactFinalizedInstanceData { index: finalized.index, real_data }, + ) +} + +fn expand_compact_finalized_instance( + package: &CACSetupPackage, + finalized: CompactFinalizedInstanceData, +) -> Result { + let commit = package + .commits + .get(finalized.index) + .ok_or_else(|| anyhow::anyhow!("finalized index {} out of range", finalized.index))?; + if commit.epk.len() != LAMPORT_N { + bail!( + "finalized index {} has {} BABE input commitments; expected {LAMPORT_N}", + finalized.index, + commit.epk.len() + ); + } + let mut wire_hashes = Vec::with_capacity(INPUT_WIRE_NUM); + wire_hashes.extend(commit.epk[..254].iter().map(to_wire_hash)); + wire_hashes.extend(commit.wots_padding_epk[..2].iter().map(to_wire_hash)); + wire_hashes.extend(commit.epk[254..].iter().map(to_wire_hash)); + wire_hashes.extend(commit.wots_padding_epk[2..].iter().map(to_wire_hash)); + wire_hashes.extend(pubin_wire_hashes(commit)); + Ok(FinalizedInstanceData { + index: finalized.index, + final_msg_hash: commit.h_msg, + wire_hashes, + gc_commitment: commit.com_gc, + adaptor_commitment: commit.com_adaptor, + ct_setup_commitment: commit.h_ct_setup, + real_data: Some(finalized.real_data), + }) +} + +fn to_real_finalized(finalized: &FinalizedInstanceData) -> Result { + let payload = finalized.real_data.as_ref().ok_or_else(|| { + anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) + })?; + Ok(RealFinalizedInstanceData { + index: finalized.index, + gc_ciphertexts: payload.gc_ciphertexts.iter().map(|value| value.map(S)).collect(), + adaptor_table: to_real_adaptor_table(&payload.adaptor_table)?, + ct_setup: RealSetupCt { + ct2_r_delta_g2: payload.ct_setup.ct2_r_delta_g2.clone(), + ct3_masked_msg: payload.ct_setup.ct3_masked_msg.clone(), + }, + constant_labels: [S(payload.constant_labels[0]), S(payload.constant_labels[1])], + }) +} + +fn from_real_adaptor_table(table: &RealSparseAdaptorTable) -> SerializableSparseAdaptorTable { + SerializableSparseAdaptorTable { + entries: table + .entries + .iter() + .map(|entry| SerializableSparseAdaptorEntry { + x: from_real_adaptor_row(&entry.x), + y: from_real_adaptor_row(&entry.y), + z: from_real_adaptor_row(&entry.z), + }) + .collect(), + } +} + +fn from_real_adaptor_row(row: &RealSparseAdaptorRow) -> SerializableSparseAdaptorRow { + let mut offset = Vec::new(); + row.offset.serialize_compressed(&mut offset).expect("serialize adaptor offset"); + SerializableSparseAdaptorRow { cts: row.cts.clone(), offset } +} + +fn to_real_adaptor_table(table: &SerializableSparseAdaptorTable) -> Result { + Ok(RealSparseAdaptorTable { + entries: table + .entries + .iter() + .map(|entry| { + Ok(RealSparseAdaptorEntry { + x: to_real_adaptor_row(&entry.x)?, + y: to_real_adaptor_row(&entry.y)?, + z: to_real_adaptor_row(&entry.z)?, + }) + }) + .collect::>>()?, + }) +} + +fn to_real_adaptor_row(row: &SerializableSparseAdaptorRow) -> Result { + Ok(RealSparseAdaptorRow { + cts: row.cts.clone(), + offset: Fq::deserialize_compressed(row.offset.as_slice())?, + }) +} + +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_all_finalized_messages( + prover_state: &BabeProverState, + challenge_witness: &BabeChallengeAssertWitness, + proof: &ark_groth16::Proof, + real_state: &ProverSetupState, +) -> Result { + if real_state.finalized.len() != prover_state.h_msgs.len() { + bail!("BABE prover state finalized data and hash count differ"); + } + if real_state.finalized.is_empty() { + bail!("BABE prover state has no finalized instances"); + } + + let base_input_labels = pi1_labels_from_challenge(&challenge_witness.input_labels)?; + let soldered_output = &prover_state.soldering.soldered_output; + if soldered_output.base_commitment.len() != base_input_labels.len() { + bail!( + "soldering base commitment count {} does not match BABE input label count {}", + soldered_output.base_commitment.len(), + base_input_labels.len() + ); + } + + let prover = BABEProver::new(proof.clone()); + let final_msgs = real_state + .finalized + .iter() + .zip(&prover_state.h_msgs) + .enumerate() + .map(|(position, (finalized, expected_hash))| { + let input_labels = + soldered_input_labels(&base_input_labels, soldered_output, position)?; + recover_finalized_message( + &prover, + proof, + finalized, + &input_labels, + *expected_hash, + position, + ) + }) + .collect::>>()?; + + Ok(BabeWronglyChallengedWitness { + verifier_index: challenge_witness.verifier_index, + final_msgs, + }) +} + +fn pi1_labels_from_challenge(input_labels: &[[u8; 16]]) -> Result> { + if input_labels.len() == LAMPORT_N { + return Ok(input_labels.iter().copied().map(S).collect()); + } + if input_labels.len() != INPUT_WIRE_NUM { + bail!( + "challenge witness has {} input labels; expected {LAMPORT_N} or {INPUT_WIRE_NUM}", + input_labels.len() + ); + } + Ok(input_labels[..254].iter().chain(&input_labels[256..510]).copied().map(S).collect()) +} + +fn soldered_input_labels( + base_input_labels: &[S], + soldered_output: &SolderedLabelsData, + finalized_position: usize, +) -> Result> { + if finalized_position == 0 { + return Ok(base_input_labels.to_vec()); + } + let deltas = soldered_output.deltas.get(finalized_position - 1).ok_or_else(|| { + anyhow::anyhow!("missing soldering deltas for finalized position {finalized_position}") + })?; + if deltas.len() != base_input_labels.len() { + bail!( + "soldering delta count {} does not match BABE input label count {}", + deltas.len(), + base_input_labels.len() + ); + } + + Ok(base_input_labels + .iter() + .enumerate() + .map(|(wire, &base_label)| { + let (delta_false, delta_true) = deltas[wire]; + if hash32(&base_label.0) == soldered_output.base_commitment[wire].0 { + base_label ^ S(delta_false) + } else { + base_label ^ S(delta_true) + } + }) + .collect()) +} + +fn recover_finalized_message( + prover: &BABEProver, + proof: &ark_groth16::Proof, + finalized: &RealFinalizedInstanceData, + input_labels: &[S], + expected_hash: [u8; 20], + finalized_position: usize, +) -> Result> { + let mut full_labels = Vec::with_capacity(2 + input_labels.len()); + full_labels.push(finalized.constant_labels[0]); + full_labels.push(finalized.constant_labels[1]); + full_labels.extend_from_slice(input_labels); + + let (mut circuit, gc_output_indices) = verifiable_circuit_babe::gc::read_fresh_circuit(); + let ct_prove = prover.compute_ct_prove( + &mut circuit, + &gc_output_indices, + &full_labels, + &finalized.gc_ciphertexts, + &finalized.adaptor_table, + ); + let msg = BABEProver::compute_msg(proof, &ct_prove, &finalized.ct_setup) + .map_err(anyhow::Error::msg)?; + let final_msg = msg.to_vec(); + if label_hash(&final_msg) != expected_hash { + bail!( + "recovered message at finalized position {finalized_position} does not match hashlock" + ); + } + Ok(final_msg) +} + +fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature> { + wots_sig.try_into().map_err(|_| { + anyhow::anyhow!( + "WOTS signature has {} digit signatures; expected {WOTS_SIG_COUNT}", + wots_sig.len() + ) + }) +} + +fn padding_wire_hashes() -> [[[u8; 20]; 2]; 4] { + let false_hash = label_hash(&vec![0u8; 16]); + let true_hash = label_hash(&vec![1u8; 16]); + [[false_hash, true_hash]; 4] +} + +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 to_wire_hash(pair: &[[u8; 20]; 2]) -> WireHash { + WireHash { false_label_hash: pair[0], true_label_hash: pair[1] } +} + +fn pi1_to_wots96_msg(pi1: &ark_bn254::G1Affine, pubin_commitment: &[u8; 32]) -> [u8; 96] { + let mut msg = [0u8; 96]; + let mut tmp = Vec::new(); + + pi1.x.serialize_uncompressed(&mut tmp).expect("serialize pi1.x"); + msg[..32].copy_from_slice(&tmp); + + tmp.clear(); + pi1.y.serialize_uncompressed(&mut tmp).expect("serialize pi1.y"); + msg[32..64].copy_from_slice(&tmp); + + msg[64..96].copy_from_slice(pubin_commitment); + msg +} + +fn pubin_wire_hashes(commit: &CACInstanceCommit) -> Vec { + (0..256) + .map(|index| WireHash { + false_label_hash: label_hash(&pubin_label(commit, index, false).to_vec()), + true_label_hash: label_hash(&pubin_label(commit, index, true).to_vec()), + }) + .collect() +} + +fn pubin_input_labels(commit: &CACInstanceCommit, pubin_commitment: &[u8; 32]) -> Vec<[u8; 16]> { + (0..256) + .map(|index| { + let byte = pubin_commitment[index / 8]; + let bit = ((byte >> (index % 8)) & 1) == 1; + pubin_label(commit, index, bit) + }) + .collect() +} + +fn pubin_label(commit: &CACInstanceCommit, index: usize, bit: bool) -> [u8; 16] { + let mut bytes = Vec::with_capacity(32 * 4 + std::mem::size_of::() + 1); + bytes.extend_from_slice(&commit.h_ct_setup); + bytes.extend_from_slice(&commit.com_adaptor); + bytes.extend_from_slice(&commit.com_gc); + bytes.extend_from_slice(&commit.h_msg); + bytes.extend_from_slice(&(index as u64).to_le_bytes()); + bytes.push(u8::from(bit)); + hash16(&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 00000000..1911b0fc --- /dev/null +++ b/crates/bitvm-gc/src/committee/api.rs @@ -0,0 +1,615 @@ +use anyhow::{Result, bail, ensure}; +use bitcoin::XOnlyPublicKey; +use bitcoin::{PublicKey, Transaction, key::Keypair, taproot::Signature as TaprootSignature}; +use goat::contexts::base::generate_n_of_n_public_key; +use goat::transactions::base::BaseTransaction; +use goat::transactions::pre_signed_musig2::{get_nonce_message, verify_public_nonce}; +use goat::transactions::signing_musig2::generate_aggregated_nonce; +use musig2::{AggNonce, PartialSignature, PubNonce, SecNonce}; +use secp256k1::schnorr::Signature as SchnorrSignature; +use serde::{Deserialize, Serialize}; + +use crate::keys::hkdf_derive_bytes; +use crate::types::BitvmGcGraph; + +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) +} + +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 89% rename from crates/bitvm2-ga/src/keys.rs rename to crates/bitvm-gc/src/keys.rs index 6aff5164..82f7fa0d 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"), @@ -344,7 +343,7 @@ impl CommitteeMasterKey { instance_id: Uuid, graph_id: Uuid, watchtower_num: usize, - assert_commit_num: usize, + verifier_num: usize, signer_keypair: Keypair, ) -> (CommitteePubNonces, CommitteeSecNonces, CommitteeNonceSignatures) { let domain = [ @@ -359,7 +358,7 @@ impl CommitteeMasterKey { graph_id.as_u128() as usize, signer_keypair, watchtower_num, - assert_commit_num, + verifier_num, ) } @@ -391,13 +390,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 +415,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 +449,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 +579,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 +603,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 +630,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 77% rename from crates/bitvm2-ga/src/lib.rs rename to crates/bitvm-gc/src/lib.rs index faa1e805..3e8ed2ee 100644 --- a/crates/bitvm2-ga/src/lib.rs +++ b/crates/bitvm-gc/src/lib.rs @@ -1,13 +1,12 @@ -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 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 00000000..66ff9e66 --- /dev/null +++ b/crates/bitvm-gc/src/operator/api.rs @@ -0,0 +1,597 @@ +use anyhow::{Result, bail}; +use bitcoin::{Address, Amount, Transaction, TxIn, key::Keypair}; +use bitcoin::{Network, PublicKey, Witness}; +use goat::assert_scripts::{ + OperatorAssertPublicKey, OperatorAssertSecretKey, OperatorCommitPubinPublicKey, + OperatorCommitPubinSecretKey, +}; +use goat::constants::{CONNECTOR_A_TIMELOCK, CONNECTOR_D_TIMELOCK}; +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::utils::num_blocks_per_network; +use goat::wots::{Wots, Wots96}; + +use crate::keys::hkdf_derive_bytes; +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 { + 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(()) +} + +/// 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], +) -> 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) + .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 { + num_blocks_per_network(network, CONNECTOR_A_TIMELOCK) +} + +pub fn take2_timelock(network: Network) -> u32 { + num_blocks_per_network(network, CONNECTOR_D_TIMELOCK) +} 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/types.rs b/crates/bitvm-gc/src/types.rs new file mode 100644 index 00000000..8061c6fb --- /dev/null +++ b/crates/bitvm-gc/src/types.rs @@ -0,0 +1,725 @@ +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}, + }, + 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 uuid::Uuid; + +use crate::{ + committee::{CommitteeSignatures, push_committee_pre_signatures}, + operator::{generate_bitvm_graph, push_operator_pre_signature}, +}; + +#[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 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(), + ) + } + + 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(), + ) + } + + 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, + ) + } + + pub fn connector_e(&self) -> ConnectorE { + ConnectorE::new( + self.network(), + &self.n_of_n_taproot_public_key(), + &self.operator_commit_pubin_wots_pubkey, + ) + } + + pub fn connector_f(&self) -> ConnectorF { + ConnectorF::new( + self.network(), + &self.operator_taproot_public_key(), + &self.n_of_n_taproot_public_key(), + ) + } + + 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, + )) + } + + pub fn watchtower_challenge_connectors(&self) -> Vec { + self.watchtower_pubkeys + .iter() + .map(|pubkey| { + WatchtowerChallengeConnector::new( + self.network(), + &self.operator_taproot_public_key(), + pubkey, + ) + }) + .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)) + } + + 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) + }) + .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(), + )) + } + + 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(), + ) + }) + .collect() + } + + 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) + } +} + +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 00000000..5895c0aa --- /dev/null +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -0,0 +1,219 @@ +use crate::types::BitvmGcGraph; +use anyhow::{Result, bail}; +use bitcoin::{Address, Amount, Network, ScriptBuf, Transaction, TxIn, TxOut}; +use bitvm::chunk::api::type_conversion_utils::RawWitness; +use goat::{ + assert_scripts::{INPUT_WIRE_NUM, Label}, + connectors::{base::TaprootConnector, connector_d::CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX}, + constants::PROVER_CONNECTOR_TIMELOCK, + scripts::{generate_opreturn_script, p2a_output}, + transactions::{ + assert::{pubin_disprove, validate_pubin}, + base::DUST_AMOUNT, + pre_signed::PreSignedTransaction, + }, + utils::num_blocks_per_network, +}; + +/// 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)) +} + +pub fn verify_prover_assertion(_graph: &BitvmGcGraph, _operator_assert_txin: TxIn) -> Result { + todo!("verify operator assertion") +} + +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 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, + ack_preimages: Vec>, +) -> Result> { + 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 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 { + num_blocks_per_network(network, PROVER_CONNECTOR_TIMELOCK) +} 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 00000000..9ce8d1a4 --- /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 00000000..19af1c2a --- /dev/null +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -0,0 +1,269 @@ +use ark_bn254::Fr; +use ark_crypto_primitives::snark::CircuitSpecificSetupSNARK; +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, + CACInstanceCommit, CACSetupPackage, 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, verify_real_setup, + verify_setup, +}; +use bitvm_gc::operator::generate_wots_key; +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 GC_GATES_PATH and GC_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 public_inputs = vec![a * b]; + + let (package, private_state) = + build_real_setup_package(BABE_M_CC, &vk, &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, + &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, + &public_inputs, + ) + .expect("verify soldering proof"); + extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + .expect("extract native 508-wire graph data"); +} + +#[test] +fn babe_setup_payload_round_trips_and_derives_gc_data() { + let package = CACSetupPackage { + commits: (0..BABE_M_CC).map(|index| CACInstanceCommit::sample(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| FinalizedInstanceData::sample(*index)) + .collect::>(); + let soldering = SolderingData::sample(finalized_indices); + let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + .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() { + let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); + let soldering = SolderingData::sample((0..BABE_M_CC).collect()); + + let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + .expect("one verifier graph slot"); + + assert_eq!( + gc_data.final_msg_hashlocks, + finalized.iter().map(|data| data.final_msg_hash).collect::>() + ); + assert!(gc_data.wire_hashes.as_slice() == finalized[0].wire_hashes.as_slice()); +} + +#[test] +fn gc_slot_rejects_invalid_finalized_counts_and_soldering_order() { + for count in [1, 3, 5, 8] { + let finalized = (0..count).map(FinalizedInstanceData::sample).collect::>(); + let soldering = SolderingData::sample((0..count).collect()); + let error = match extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) { + Ok(_) => panic!("invalid finalized count"), + Err(error) => error, + }; + assert!(error.to_string().contains(&format!("exactly {BABE_M_CC} finalized"))); + } + + let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); + let mut indices = (0..BABE_M_CC).collect::>(); + indices.swap(0, 1); + let error = match extract_gc_circuit_data( + &finalized, + &SolderingData::sample(indices), + verifier_pubkey(), + ) { + Ok(_) => panic!("mismatched soldering order"), + Err(error) => error, + }; + assert!(error.to_string().contains("soldering finalized indices mismatch")); +} + +#[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"); + let second = derive_finalized_indices(&package, 4).expect("derive full cut again"); + assert_eq!(first, second); + 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() { + let (assert_secret_key, _) = generate_wots_key("graph-scoped-key"); + let proof = ark_groth16::Proof::::default(); + let assert_witness = build_assert_witness(&proof, &assert_secret_key).expect("assert witness"); + assert_eq!(assert_witness.wots_sig.len(), WOTS_SIG_COUNT); + assert!(!assert_witness.pi1.is_empty()); + assert!(build_assert_witness(&proof, &Vec::new()).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.input_labels.len(), INPUT_WIRE_NUM); + + let final_msgs = (0..BABE_M_CC) + .map(|index| format!("finalized-preimage-{index}").into_bytes()) + .collect::>(); + let h_msgs = final_msgs.iter().map(label_hash).collect::>(); + let prover_state = BabeProverState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized: (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect(), + soldering: SolderingData::sample((0..BABE_M_CC).collect()), + h_msgs, + }; + let wrongly_challenged = + build_wrongly_challenged_witness(&prover_state, &challenge_witness, final_msgs.clone()) + .expect("wrongly challenged witness"); + assert_eq!(wrongly_challenged.verifier_index, 12); + assert_eq!(wrongly_challenged.final_msgs, final_msgs); + assert!( + build_wrongly_challenged_witness( + &prover_state, + &challenge_witness, + vec![b"missing-preimage".to_vec(); BABE_M_CC], + ) + .is_err() + ); +} + +#[test] +fn wrongly_challenged_witness_requires_all_finalized_preimages() { + let final_msgs = (0..BABE_M_CC) + .map(|index| format!("finalized-preimage-{index}").into_bytes()) + .collect::>(); + let h_msgs = final_msgs.iter().map(label_hash).collect::>(); + let challenge_witness = + BabeChallengeAssertWitness { verifier_index: 0, input_labels: vec![], wots_sig: vec![] }; + + let from_preimages = build_wrongly_challenged_witness_from_preimages( + &h_msgs, + &challenge_witness, + final_msgs.clone(), + ) + .expect("wrongly challenged witness"); + assert_eq!(from_preimages.verifier_index, 0); + assert_eq!(from_preimages.final_msgs, final_msgs); + + let prover_state = BabeProverState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized: (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect(), + soldering: SolderingData::sample((0..BABE_M_CC).collect()), + h_msgs, + }; + let delegated = build_wrongly_challenged_witness( + &prover_state, + &challenge_witness, + from_preimages.final_msgs.clone(), + ) + .expect("delegated wrongly challenged witness"); + assert_eq!(delegated, from_preimages); + + assert!( + build_wrongly_challenged_witness_from_preimages( + &prover_state.h_msgs, + &challenge_witness, + vec![from_preimages.final_msgs[0].clone(); BABE_M_CC], + ) + .is_err() + ); +} diff --git a/crates/bitvm2-ga/src/challenger/api.rs b/crates/bitvm2-ga/src/challenger/api.rs deleted file mode 100644 index 30287c60..00000000 --- a/crates/bitvm2-ga/src/challenger/api.rs +++ /dev/null @@ -1,278 +0,0 @@ -use crate::types::{Bitvm2Graph, VerifyingKey}; -use anyhow::{Result, bail}; -use bitcoin::{Address, Amount, Network, ScriptBuf, Transaction, TxIn, TxOut, XOnlyPublicKey}; -use bitvm::chunk::api::{ - NUM_HASH, NUM_PUBS, NUM_TAPS, NUM_U256, type_conversion_utils::RawWitness, -}; -use goat::{ - connectors::{ - assert_connectors::{extract_commits_from_txin, extract_commits_from_txins}, - connector_e::ConnectorE, - }, - constants::{ACK_TIMELOCK, ASSERT_COMMIT_TIMELOCK, CONNECTOR_G_TIMELOCK}, - disprove_scripts::{GUEST_VALIDATION_TAPS, NUM_GUEST_PUBS_ASSERT, NUM_GUEST_PUBS_EXTRA}, - scripts::{generate_opreturn_script, p2a_output}, - transactions::{ - base::{DUST_AMOUNT, Input}, - disprove::{disprove, validate_assert}, - pre_signed::PreSignedTransaction, - watchtower_challenge::extract_operator_preimage_from_ack_txin, - }, - utils::num_blocks_per_network, -}; - -pub fn extract_blockhash_commit_witness( - operator_commit_blockhash_txin: &TxIn, -) -> Result> { - match extract_commits_from_txin(operator_commit_blockhash_txin, NUM_GUEST_PUBS_EXTRA, 0) { - Ok(v) => Ok(v), - Err(e) => bail!("Failed to extract blockhash commit witness: {e}"), - } -} - -pub fn extract_assert_commit_witness( - operator_assert_commit_txins: Vec, -) -> Result> { - match extract_commits_from_txins( - operator_assert_commit_txins, - NUM_GUEST_PUBS_ASSERT + NUM_PUBS + NUM_U256, - NUM_HASH, - ) { - Ok(v) => Ok(v), - Err(e) => bail!("Failed to extract assert commit witness: {e}"), - } -} - -/// return (if any) disprove witness -pub fn verify_operator_commits( - operator_commit_blockhash_txin: TxIn, - operator_assert_commit_txins: Vec, - operator_ack_txins: Vec, - watchtower_num: usize, - vk: &VerifyingKey, - disprove_scripts: &[ScriptBuf; GUEST_VALIDATION_TAPS + NUM_TAPS], -) -> Result> { - let mut preimages = vec![vec![]; watchtower_num]; - for txin in &operator_ack_txins { - let watchtower_index = txin.previous_output.vout as usize / 2; - if watchtower_index >= watchtower_num || txin.previous_output.vout % 2 != 1 { - bail!( - "invalid ack txin in operator_ack_txins, unexpected vout: {}", - txin.previous_output.vout - ); - } - let preimage = extract_operator_preimage_from_ack_txin(txin) - .map_err(|e| anyhow::anyhow!("Failed to extract preimage from ack txin: {e}"))?; - preimages[watchtower_index] = preimage; - } - let (guest_validation_scripts, proof_validation_scripts) = - disprove_scripts.split_at(GUEST_VALIDATION_TAPS); - let guest_validation_scripts = - <&[ScriptBuf; GUEST_VALIDATION_TAPS]>::try_from(guest_validation_scripts).unwrap(); - let proof_validation_scripts = - <&[ScriptBuf; NUM_TAPS]>::try_from(proof_validation_scripts).unwrap(); - let res = validate_assert( - extract_blockhash_commit_witness(&operator_commit_blockhash_txin)?, - extract_assert_commit_witness(operator_assert_commit_txins)?, - preimages, - guest_validation_scripts, - vk, - proof_validation_scripts, - ); - if let Some((_, scr)) = &res { - let guest_index_opt = guest_validation_scripts.iter().position(|s| s == scr); - if let Some(guest_index) = guest_index_opt { - tracing::info!( - "Disprove witness validated against guest validation scripts at index {}", - guest_index - ); - } else { - let proof_index_opt = proof_validation_scripts.iter().position(|s| s == scr); - match proof_index_opt { - Some(idx) => tracing::info!( - "Disprove witness validated against proof validation scripts at index {}", - idx - ), - None => tracing::warn!( - "Disprove witness script not found in either guest or proof validation scripts" - ), - } - } - } - Ok(res) -} - -/// 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: &Bitvm2Graph) -> Result<(Transaction, Amount)> { - if !graph.operator_pre_signed() { - bail!("missing pre-signatures from operator") - }; - Ok((graph.challenge.tx().clone(), graph.challenge.challenge_amount)) -} - -/// disprove has a huge disprove input and an optional op_return output -/// get incomplete tx here, add inputs with enough amount, then broadcast it to finish challnege progress -pub fn sign_disprove( - graph: &Bitvm2Graph, - connector_e_input: &Input, - disprove_witness: (RawWitness, ScriptBuf), - disprove_scripts: Vec, - disprover_evm_address: Option<[u8; 20]>, -) -> Result { - if !graph.committee_pre_signed() { - bail!("missing pre-signatures from committee") - }; - let network = graph.parameters.instance_parameters.network; - let operator_pubkey = graph.parameters.operator_pubkey; - let operator_taproot_public_key = XOnlyPublicKey::from(operator_pubkey); - let (_, connector_e_taproot_spend_info) = - ConnectorE::new_with_scripts(network, &operator_taproot_public_key, disprove_scripts); - let (input_script_witness, input_lock_script) = disprove_witness; - let disprove_txin = disprove( - &connector_e_taproot_spend_info, - connector_e_input, - input_script_witness, - input_lock_script, - ) - .map_err(|e| anyhow::anyhow!("Failed to create disprove txin: {e}"))?; - let mut disprove_tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![disprove_txin], - output: vec![], - }; - - // write challenger's l2 address to an op_return output - if let Some(disprover_evm_address) = disprover_evm_address { - disprove_tx.output.push(TxOut { - value: Amount::ZERO, - script_pubkey: generate_opreturn_script(disprover_evm_address.to_vec()), - }); - } - - Ok(disprove_tx) -} - -/// 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: &Bitvm2Graph, - challenger_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, - challenger_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: &Bitvm2Graph, - challenger_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, - challenger_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: &Bitvm2Graph, - challenger_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, - challenger_receive_address, - fee_rate, - )?; - Ok((tx, anchor_added)) -} - -// nack, commit_blockhash_timeout, assert_commit_timeout are already fully signed. -// Just wait for their timelocks to expire, then broadcast them and cover fees via CPFP. - -pub fn nack_timelock(network: Network) -> u32 { - num_blocks_per_network(network, ACK_TIMELOCK) // actual delay on bitcoin network - + if network == Network::Testnet { 18 } else { 0 } // Testnet extra delay - + if network == Network::Testnet4 { 40 } else { 0 } // Testnet4 extra delay - + if network == Network::Regtest { 4 } else { 0 } // Regtest extra delay -} - -pub fn commit_blockhash_timeout_timelock(network: Network) -> u32 { - num_blocks_per_network(network, CONNECTOR_G_TIMELOCK) - + if network == Network::Testnet { 18 } else { 0 } // Testnet extra delay - + if network == Network::Testnet4 { 40 } else { 0 } // Testnet4 extra delay - + if network == Network::Regtest { 4 } else { 0 } // Regtest extra delay -} - -pub fn assert_commit_timeout_timelock(network: Network) -> u32 { - num_blocks_per_network(network, ASSERT_COMMIT_TIMELOCK) - + if network == Network::Testnet4 { 40 } else { 0 } // Testnet4 extra delay - + if network == Network::Regtest { 6 } else { 0 } // Regtest extra delay -} diff --git a/crates/bitvm2-ga/src/committee/api.rs b/crates/bitvm2-ga/src/committee/api.rs deleted file mode 100644 index e92eb8cc..00000000 --- a/crates/bitvm2-ga/src/committee/api.rs +++ /dev/null @@ -1,682 +0,0 @@ -use crate::keys::hkdf_derive_bytes; -use crate::types::Bitvm2Graph; -use anyhow::{Result, bail}; -use bitcoin::{PublicKey, Transaction, XOnlyPublicKey}; -use bitcoin::{key::Keypair, taproot::Signature as TaprootSignature}; -use goat::connectors::assert_connectors::generate_chunked_assert_commit_connectors; -use goat::connectors::connector_0::Connector0; -use goat::connectors::connector_a::ConnectorA; -use goat::connectors::connector_d::ConnectorD; -use goat::connectors::connector_f::ConnectorF; -use goat::connectors::connector_g::ConnectorG; -use goat::connectors::connector_z::ConnectorZ; -use goat::connectors::watchtower_connectors::{ - AckConnector, WatchctowerConnectors, WatchtowerChallengeConnector, -}; -use goat::contexts::base::generate_n_of_n_public_key; -use goat::transactions::pre_signed_musig2::{get_nonce_message, verify_public_nonce}; -use goat::transactions::{base::BaseTransaction, signing_musig2::generate_aggregated_nonce}; -use musig2::{AggNonce, PartialSignature, PubNonce, SecNonce}; -use secp256k1::schnorr::Signature as SchnorrSignature; -use serde::{Deserialize, Serialize}; - -const COMMITTEE_NONCE_HKDF_SALT: &[u8] = b"bitvm2/committee-nonce/v1"; - -pub fn key_aggregation(pubkeys: &[PublicKey]) -> PublicKey { - generate_n_of_n_public_key(pubkeys).0 -} - -pub fn take1_pre_sign_num() -> usize { - 1 -} -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 nack_pre_sign_num(watchtower_num: usize) -> usize { - 2 * watchtower_num -} -pub fn blockhash_commit_timeout_pre_sign_num() -> usize { - 2 -} -pub fn assert_commit_timeout_pre_sign_num(assert_commit_num: usize) -> usize { - 2 * assert_commit_num -} - -pub fn sign_pegin_confirm( - graph: &Bitvm2Graph, - 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 verifier_context = - graph.parameters.instance_parameters.get_verifier_context(committee_member_keypair)?; - pegin_confirm - .sign_input_0_musig2(&verifier_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: &Bitvm2Graph, - 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 = ConnectorZ::new( - graph.parameters.instance_parameters.network, - &XOnlyPublicKey::from(graph.parameters.instance_parameters.committee_agg_pubkey), - &graph.parameters.instance_parameters.user_info.user_xonly_pubkey, - ); - pegin_confirm.push_input_0_signature(&connector_z, agg_sig); - Ok(pegin_confirm.finalize()) -} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] -pub struct CommitteeMusig2Data { - pub take1: Vec, - pub take2: Vec, - pub challenge: Vec, - pub watchtower_challenge_timeout: Vec, - pub nack: Vec, - pub blockhash_commit_timeout: Vec, - pub assert_commit_timeout: 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, assert_commit_num: usize) -> Result<()> { - if self.take1.len() != take1_pre_sign_num() { - bail!("invalid number of take1"); - } - if self.take2.len() != take2_pre_sign_num() { - bail!("invalid number of take2"); - } - if self.challenge.len() != challenge_pre_sign_num() { - bail!("invalid number of challenge"); - } - if self.blockhash_commit_timeout.len() != blockhash_commit_timeout_pre_sign_num() { - bail!("invalid number of blockhash_commit_timeout"); - } - if self.watchtower_challenge_timeout.len() - != watchtower_challenge_timeout_pre_sign_num(watchtower_num) - { - bail!("invalid number of watchtower_challenge_timeout"); - } - if self.nack.len() != nack_pre_sign_num(watchtower_num) { - bail!("invalid number of nack"); - } - if self.assert_commit_timeout.len() != assert_commit_timeout_pre_sign_num(assert_commit_num) - { - bail!("invalid number of assert_commit_timeout"); - } - Ok(()) - } - - pub fn new_empty() -> Self { - CommitteeMusig2Data { - take1: vec![], - take2: vec![], - challenge: vec![], - watchtower_challenge_timeout: vec![], - nack: vec![], - blockhash_commit_timeout: vec![], - assert_commit_timeout: vec![], - } - } -} - -pub fn committee_pre_sign( - committee_member_keypair: Keypair, - committee_member_sec_nonce: CommitteeSecNonces, - committee_agg_nonce: CommitteeAggNonces, - graph: &mut Bitvm2Graph, -) -> Result { - let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let assert_commit_num = graph.assert_commit_timeout_txns.len(); - committee_member_sec_nonce.validate_length(watchtower_num, assert_commit_num)?; - committee_agg_nonce.validate_length(watchtower_num, assert_commit_num)?; - - let verifier_context = - graph.parameters.instance_parameters.get_verifier_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(&verifier_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(&verifier_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(&verifier_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 watchtower_challenge_timeout_sigs = vec![]; - for (i, watchtower_challenge_timeout_tx) in - graph.watchtower_challenge_timeout_txns.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 = watchtower_challenge_timeout_tx - .pre_sign(&verifier_context, &sec_nonces, &agg_nonces) - .map_err(|e| { - anyhow::anyhow!( - "fail to pre-sign {}: {e}", - watchtower_challenge_timeout_tx.name() - ) - })?; - watchtower_challenge_timeout_sigs.extend(sigs); - } - res.watchtower_challenge_timeout = watchtower_challenge_timeout_sigs; - } - - { - // nack - let mut nack_sigs = vec![]; - for (i, nack_tx) in graph.nack_txns.iter_mut().enumerate() { - let sec_nonces = [ - committee_member_sec_nonce.nack[i * 2].clone(), - committee_member_sec_nonce.nack[i * 2 + 1].clone(), - ]; - let agg_nonces = [ - committee_agg_nonce.nack[i * 2].clone(), - committee_agg_nonce.nack[i * 2 + 1].clone(), - ]; - let sigs = nack_tx - .pre_sign(&verifier_context, &sec_nonces, &agg_nonces) - .map_err(|e| anyhow::anyhow!("fail to pre-sign {}: {e}", nack_tx.name()))?; - nack_sigs.extend(sigs); - } - res.nack = nack_sigs; - } - - { - // blockhash-commit-timeout - let sec_nonces = committee_member_sec_nonce.blockhash_commit_timeout.try_into().unwrap(); - let agg_nonces = committee_agg_nonce.blockhash_commit_timeout.try_into().unwrap(); - match graph.blockhash_commit_timeout.pre_sign(&verifier_context, &sec_nonces, &agg_nonces) { - Ok(v) => res.blockhash_commit_timeout = v.to_vec(), - Err(e) => bail!("fail to pre-sign {}: {e}", graph.blockhash_commit_timeout.name()), - } - } - - { - // assert-commit-timeout - let mut assert_commit_timeout_sigs = vec![]; - for (i, assert_commit_timeout_tx) in graph.assert_commit_timeout_txns.iter_mut().enumerate() - { - let sec_nonces = [ - committee_member_sec_nonce.assert_commit_timeout[i * 2].clone(), - committee_member_sec_nonce.assert_commit_timeout[i * 2 + 1].clone(), - ]; - let agg_nonces = [ - committee_agg_nonce.assert_commit_timeout[i * 2].clone(), - committee_agg_nonce.assert_commit_timeout[i * 2 + 1].clone(), - ]; - let sigs = assert_commit_timeout_tx - .pre_sign(&verifier_context, &sec_nonces, &agg_nonces) - .map_err(|e| { - anyhow::anyhow!("fail to pre-sign {}: {e}", assert_commit_timeout_tx.name()) - })?; - assert_commit_timeout_sigs.extend(sigs); - } - res.assert_commit_timeout = assert_commit_timeout_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 - })?, - nack: aggregate_field(pub_nonces_vec, |c| &c.nack)?, - blockhash_commit_timeout: aggregate_field(pub_nonces_vec, |c| &c.blockhash_commit_timeout)?, - assert_commit_timeout: aggregate_field(pub_nonces_vec, |c| &c.assert_commit_timeout)?, - }) -} - -pub fn signature_aggregation( - partial_sigs: &Vec, - agg_nonces: &CommitteeAggNonces, - graph: &Bitvm2Graph, -) -> Result { - let context = graph.parameters.get_base_context(); - let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let assert_commit_num = graph.assert_commit_timeout_txns.len(); - agg_nonces.validate_length(watchtower_num, assert_commit_num)?; - for r in partial_sigs { - r.validate_length(watchtower_num, assert_commit_num)?; - } - - let mut res: CommitteeSignatures = CommitteeSignatures::new_empty(); - - // take1 - let take1_agg_nonces = agg_nonces.take1.clone().try_into().unwrap(); - let take1_partial_sigs = [partial_sigs.iter().map(|r| r.take1[0]).collect()]; - 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()), - }; - - // blockhash-commit-timeout - let blockhash_commit_timeout_agg_nonces = - agg_nonces.blockhash_commit_timeout.clone().try_into().unwrap(); - let mut blockhash_commit_timeout_partial_sigs = [vec![], vec![]]; - partial_sigs.iter().for_each(|r| { - blockhash_commit_timeout_partial_sigs[0].push(r.blockhash_commit_timeout[0]); - blockhash_commit_timeout_partial_sigs[1].push(r.blockhash_commit_timeout[1]); - }); - match graph.blockhash_commit_timeout.aggregate_pre_sigs( - &context, - &blockhash_commit_timeout_partial_sigs, - &blockhash_commit_timeout_agg_nonces, - ) { - Ok(v) => res.blockhash_commit_timeout = v.to_vec(), - Err(e) => { - bail!("fail to aggregate pre-sigs {}: {e}", graph.blockhash_commit_timeout.name()) - } - }; - - // watchtower_challenge_timeout - let mut watchtower_challenge_timeout_sigs = vec![]; - for (i, watchtower_challenge_timeout_tx) in - graph.watchtower_challenge_timeout_txns.iter().enumerate() - { - let _agg_nonces = [agg_nonces.watchtower_challenge_timeout[i].clone()]; - let mut _partial_sigs = [vec![]]; - partial_sigs.iter().for_each(|r| { - _partial_sigs[0].push(r.watchtower_challenge_timeout[i]); - }); - let sigs = watchtower_challenge_timeout_tx - .aggregate_pre_sigs(&context, &_partial_sigs, &_agg_nonces) - .map_err(|e| { - anyhow::anyhow!( - "fail to aggregate pre-sigs {}: {e}", - watchtower_challenge_timeout_tx.name() - ) - })?; - watchtower_challenge_timeout_sigs.extend(sigs); - } - res.watchtower_challenge_timeout = watchtower_challenge_timeout_sigs; - - // nack - let mut nack_sigs = vec![]; - for (i, nack_tx) in graph.nack_txns.iter().enumerate() { - let _agg_nonces = [agg_nonces.nack[i * 2].clone(), agg_nonces.nack[i * 2 + 1].clone()]; - let mut _partial_sigs = [vec![], vec![]]; - partial_sigs.iter().for_each(|r| { - _partial_sigs[0].push(r.nack[i * 2]); - _partial_sigs[1].push(r.nack[i * 2 + 1]); - }); - let sigs = nack_tx - .aggregate_pre_sigs(&context, &_partial_sigs, &_agg_nonces) - .map_err(|e| anyhow::anyhow!("fail to aggregate pre-sigs {}: {e}", nack_tx.name()))?; - nack_sigs.extend(sigs); - } - res.nack = nack_sigs; - - // assert-commit-timeout - let mut assert_commit_timeout_sigs = vec![]; - for (i, assert_commit_timeout_tx) in graph.assert_commit_timeout_txns.iter().enumerate() { - let _agg_nonces = [ - agg_nonces.assert_commit_timeout[i * 2].clone(), - agg_nonces.assert_commit_timeout[i * 2 + 1].clone(), - ]; - let mut _partial_sigs = [vec![], vec![]]; - partial_sigs.iter().for_each(|r| { - _partial_sigs[0].push(r.assert_commit_timeout[i * 2]); - _partial_sigs[1].push(r.assert_commit_timeout[i * 2 + 1]); - }); - let sigs = assert_commit_timeout_tx - .aggregate_pre_sigs(&context, &_partial_sigs, &_agg_nonces) - .map_err(|e| { - anyhow::anyhow!( - "fail to aggregate pre-sigs {}: {e}", - assert_commit_timeout_tx.name() - ) - })?; - assert_commit_timeout_sigs.extend(sigs); - } - res.assert_commit_timeout = assert_commit_timeout_sigs; - - Ok(res) -} - -pub fn push_committee_pre_signatures( - graph: &mut Bitvm2Graph, - sigs: &CommitteeSignatures, -) -> Result<()> { - let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let assert_commit_num = graph.assert_commit_timeout_txns.len(); - if graph.committee_pre_signed { - bail!("already pre-signed by committee".to_string()) - }; - sigs.validate_length(watchtower_num, assert_commit_num)?; - - let network = graph.parameters.instance_parameters.network; - let n_of_n_taproot_public_key = - XOnlyPublicKey::from(graph.parameters.instance_parameters.committee_agg_pubkey); - let operator_taproot_public_key = XOnlyPublicKey::from(graph.parameters.operator_pubkey); - let connector_0 = Connector0::new(network, &n_of_n_taproot_public_key); - let connector_a = - ConnectorA::new(network, &operator_taproot_public_key, &n_of_n_taproot_public_key); - let connector_d = - ConnectorD::new(network, &operator_taproot_public_key, &n_of_n_taproot_public_key); - let blockhash_wots_pubkey = &graph.parameters.operator_wots_pubkeys.0[0]; - let connector_f = - ConnectorF::new(network, &operator_taproot_public_key, &n_of_n_taproot_public_key); - let connector_g = ConnectorG::new( - network, - &n_of_n_taproot_public_key, - &operator_taproot_public_key, - blockhash_wots_pubkey, - ); - let watchtower_connectors_array = (0..watchtower_num) - .map(|i| { - ( - WatchtowerChallengeConnector::new( - network, - &operator_taproot_public_key, - &graph.parameters.watchtower_pubkeys[i], - ), - AckConnector::new( - network, - &n_of_n_taproot_public_key, - &graph.parameters.hashlocks[i], - ), - ) - }) - .collect::>(); - let assert_wots_pubkeys = - (graph.parameters.operator_wots_pubkeys.1, *graph.parameters.operator_wots_pubkeys.2); - let assert_commit_connectors = generate_chunked_assert_commit_connectors( - network, - &n_of_n_taproot_public_key, - assert_wots_pubkeys, - ); - - // take1 - graph.take1.push_pre_sigs(&connector_0, 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()); - - // blockhash-commit-timeout - graph.blockhash_commit_timeout.push_pre_sigs( - &connector_g, - &connector_f, - sigs.blockhash_commit_timeout.clone().try_into().unwrap(), - ); - - // watchtower_challenge_timeout - for (i, watchtower_challenge_timeout_tx) in - graph.watchtower_challenge_timeout_txns.iter_mut().enumerate() - { - watchtower_challenge_timeout_tx.push_pre_sigs( - &watchtower_connectors_array[i], - sigs.watchtower_challenge_timeout[i..(i + 1)].try_into().unwrap(), - ); - } - - // nack - for (i, nack_tx) in graph.nack_txns.iter_mut().enumerate() { - nack_tx.push_pre_sigs( - &watchtower_connectors_array[i], - &connector_f, - sigs.nack[i * 2..(i * 2 + 2)].try_into().unwrap(), - ); - } - - // assert-commit-timeout - for (i, assert_commit_timeout_tx) in graph.assert_commit_timeout_txns.iter_mut().enumerate() { - assert_commit_timeout_tx.push_pre_sigs( - &assert_commit_connectors[i], - &connector_d, - sigs.assert_commit_timeout[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, - assert_commit_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; - } - } - { - // nack - for _ in 0..nack_pre_sign_num(watchtower_num) { - let (sec_nonce, pub_nonce, nonce_sig) = - generate_nonce(signer_keypair, &graph_seed, index); - pub_nonces.nack.push(pub_nonce); - sec_nonces.nack.push(sec_nonce); - nonce_sigs.nack.push(nonce_sig); - index += 1; - } - } - { - // blockhash-commit-timeout - for _ in 0..blockhash_commit_timeout_pre_sign_num() { - let (sec_nonce, pub_nonce, nonce_sig) = - generate_nonce(signer_keypair, &graph_seed, index); - pub_nonces.blockhash_commit_timeout.push(pub_nonce); - sec_nonces.blockhash_commit_timeout.push(sec_nonce); - nonce_sigs.blockhash_commit_timeout.push(nonce_sig); - index += 1; - } - } - { - // assert-commit-timeout - for _ in 0..assert_commit_timeout_pre_sign_num(assert_commit_num) { - let (sec_nonce, pub_nonce, nonce_sig) = - generate_nonce(signer_keypair, &graph_seed, index); - pub_nonces.assert_commit_timeout.push(pub_nonce); - sec_nonces.assert_commit_timeout.push(sec_nonce); - nonce_sigs.assert_commit_timeout.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, - assert_commit_num: usize, -) -> Result { - pub_nonces.validate_length(watchtower_num, assert_commit_num)?; - nonce_sigs.validate_length(watchtower_num, assert_commit_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.nack, &nonce_sigs.nack) - && verify_vec( - pubkey, - &pub_nonces.blockhash_commit_timeout, - &nonce_sigs.blockhash_commit_timeout, - ) - && verify_vec(pubkey, &pub_nonces.assert_commit_timeout, &nonce_sigs.assert_commit_timeout)) -} - -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/operator/api.rs b/crates/bitvm2-ga/src/operator/api.rs deleted file mode 100644 index b556e2ee..00000000 --- a/crates/bitvm2-ga/src/operator/api.rs +++ /dev/null @@ -1,980 +0,0 @@ -use crate::keys::hkdf_derive_bytes; -use crate::types::{ - Bitvm2Graph, Bitvm2GraphParameters, Groth16Proof, OperatorWotsPublicKeys, - OperatorWotsSecretKeys, OperatorWotsSignatures, PublicInputs, VerifyingKey, -}; -use anyhow::{Result, bail}; -use bitcoin::{Address, Amount, Network, PublicKey, ScriptBuf, Transaction, TxIn}; -use bitcoin::{OutPoint, Witness, XOnlyPublicKey, key::Keypair}; -use bitvm::chunk::api::{ - NUM_HASH, NUM_PUBS, NUM_U256, PublicKeys as Groth16WotsPublicKeys, - api_generate_full_tapscripts, api_generate_partial_script, generate_assertions, -}; -use bitvm::signatures::{HASH_LEN, WinternitzSecret, Wots, Wots16, Wots32}; -use goat::connectors::assert_connectors::generate_chunked_assert_commit_connectors; -use goat::connectors::connector_0::Connector0; -use goat::connectors::connector_a::ConnectorA; -use goat::connectors::connector_b::ConnectorB; -use goat::connectors::connector_c::ConnectorC; -use goat::connectors::connector_d::ConnectorD; -use goat::connectors::connector_e::ConnectorE; -use goat::connectors::connector_f::ConnectorF; -use goat::connectors::connector_g::ConnectorG; -use goat::connectors::kickoff_connectors::{ - ForceSkipConnector, GuardianConnector, KickoffConnector, PrekickoffConnector, -}; -use goat::connectors::watchtower_connectors::{ - AckConnector, WatchctowerConnectors, WatchtowerChallengeConnector, -}; -use goat::constants::{ - CONNECTOR_A_TIMELOCK, CONNECTOR_D_TIMELOCK, CONNECTOR_F_TIMELOCK, WATCHTOWER_CHALLENGE_TIMELOCK, -}; -use goat::disprove_scripts::{ - ChallengeHashType, NUM_GUEST, NUM_GUEST_PUBS_ASSERT, NUM_GUEST_PUBS_EXTRA, hash160, - verify_guest_pubin, -}; -use goat::transactions::assert::{ - AssertCommitTimeoutTransaction, AssertInitTransaction, operator_commit_proof, -}; -use goat::transactions::base::{DUST_AMOUNT, Input}; -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::{ - BlockhashCommitTimeoutTransaction, NackTransaction, WatchtowerChallengeInitTransaction, - WatchtowerChallengeTimeoutTransaction, operator_ack, operator_commit_blockhash, -}; -use goat::utils::num_blocks_per_network; -use hex::encode as hex_encode; - -const OPERATOR_WOTS_HKDF_SALT: &[u8] = b"bitvm2/operator-wots/v1"; - -pub fn generate_wots_keys(seed: &str) -> (OperatorWotsSecretKeys, OperatorWotsPublicKeys) { - let secrets = wots_seed_to_secrets(seed); - let pubkeys = wots_secrets_to_pubkeys(&secrets); - (secrets, pubkeys) -} - -pub fn operator_presig_num() -> usize { - 6 -} - -#[allow(deprecated)] -pub fn wots_secrets_to_pubkeys(secrets: &OperatorWotsSecretKeys) -> OperatorWotsPublicKeys { - let mut index = 0; - - let mut guest_extra = vec![]; - for _ in 0..NUM_GUEST_PUBS_EXTRA { - guest_extra.push(Wots32::generate_public_key(&secrets[index])); - index += 1; - } - - let mut guest_assert = vec![]; - for _ in 0..NUM_GUEST_PUBS_ASSERT { - guest_assert.push(Wots32::generate_public_key(&secrets[index])); - index += 1; - } - - let mut pubins = vec![]; - for _ in 0..NUM_PUBS { - pubins.push(Wots32::generate_public_key(&secrets[index])); - index += 1; - } - let mut fq_arr = vec![]; - for _ in 0..NUM_U256 { - fq_arr.push(Wots32::generate_public_key(&secrets[index])); - index += 1; - } - let mut h_arr = vec![]; - for _ in 0..NUM_HASH { - h_arr.push(Wots16::generate_public_key(&secrets[index])); - index += 1; - } - - let g16_wotspubkey: Groth16WotsPublicKeys = - (pubins.try_into().unwrap(), fq_arr.try_into().unwrap(), h_arr.try_into().unwrap()); - (guest_extra.try_into().unwrap(), guest_assert.try_into().unwrap(), Box::new(g16_wotspubkey)) -} - -#[allow(deprecated)] -pub fn wots_seed_to_secrets(seed: &str) -> OperatorWotsSecretKeys { - let seed_bytes = seed.as_bytes(); - let wot32_seckeys = (0..NUM_GUEST + NUM_PUBS + NUM_U256) - .map(|idx| { - let sec_i = hex_encode(hkdf_derive_bytes( - seed_bytes, - OPERATOR_WOTS_HKDF_SALT, - format!("wots32/{idx}").as_bytes(), - 32, - )); - let sec_str = format!("{sec_i}{:04x}{:04x}", 1, idx); - Wots32::secret_from_str(&sec_str) - }) - .collect::>(); - let wot16_seckeys = (0..NUM_HASH) - .map(|idx| { - let sec_i = hex_encode(hkdf_derive_bytes( - seed_bytes, - OPERATOR_WOTS_HKDF_SALT, - format!("wots16/{idx}").as_bytes(), - 32, - )); - let sec_str = format!("{sec_i}{:04x}{:04x}", 0, idx); - Wots16::secret_from_str(&sec_str) - }) - .collect::>(); - - Box::new([wot32_seckeys, wot16_seckeys].concat().try_into().unwrap()) -} - -pub fn generate_partial_scripts(ark_vkey: &VerifyingKey) -> Vec { - api_generate_partial_script(ark_vkey) -} - -pub fn generate_disprove_scripts( - partial_scripts: &[ScriptBuf], - wots_pubkeys: OperatorWotsPublicKeys, - guest_constant_value: &[u8; 32], - watchtower_hashlocks: &Vec, -) -> (Vec, Vec) { - let (guest_pubkeys_0, guest_pubkeys_1, proof_pubkeys) = wots_pubkeys; - let mut guest_pubin_wots_pubkeys = guest_pubkeys_0.to_vec(); - guest_pubin_wots_pubkeys.extend(guest_pubkeys_1); - let guest_pubin_wots_pubkeys = guest_pubin_wots_pubkeys.try_into().unwrap(); - let guest_pubin_scripts = verify_guest_pubin( - &guest_pubin_wots_pubkeys, - &proof_pubkeys.0, - guest_constant_value, - watchtower_hashlocks, - ); - let guest_pubin_scripts = guest_pubin_scripts.into_iter().map(|s| s.compile()).collect(); - let proof_scripts = api_generate_full_tapscripts(*proof_pubkeys, partial_scripts); - (guest_pubin_scripts, proof_scripts) -} - -#[allow(deprecated)] -pub fn corrupt_proof( - sigs: &mut OperatorWotsSignatures, - wots_sec: &OperatorWotsSecretKeys, - index: usize, -) { - let mut scramble: [u8; 32] = [1u8; 32]; - scramble[16] = 37; - let mut scramble2: [u8; HASH_LEN] = [1u8; HASH_LEN]; - scramble2[HASH_LEN / 2] = 37; - println!("corrupted assertion at index {index}"); - let sec_index = index + NUM_GUEST; - if index < NUM_PUBS { - let i = index; - let assn = scramble; - let sig = Wots32::sign(&wots_sec[sec_index], &assn); - sigs.1.0[i] = sig; - } else if index < NUM_PUBS + NUM_U256 { - let i = index - NUM_PUBS; - let assn = scramble; - let sig = Wots32::sign(&wots_sec[sec_index], &assn); - sigs.1.1[i] = sig; - } else if index < NUM_PUBS + NUM_U256 + NUM_HASH { - let i = index - NUM_PUBS - NUM_U256; - let assn = scramble2; - let sig = Wots16::sign(&wots_sec[sec_index], &assn); - sigs.1.2[i] = sig; - } -} - -pub fn generate_bitvm_graph( - params: Bitvm2GraphParameters, - disprove_scripts: Vec, -) -> Result { - let network = params.instance_parameters.network; - let operator_pubkey = params.operator_pubkey; - let operator_taproot_public_key = XOnlyPublicKey::from(operator_pubkey); - let (connector_e, _) = - ConnectorE::new_with_scripts(network, &operator_taproot_public_key, disprove_scripts); - generate_bitvm_graph_inner(params, connector_e) -} - -pub(crate) fn generate_bitvm_graph_inner( - params: Bitvm2GraphParameters, - connector_e: ConnectorE, -) -> Result { - // TODO: check parameters? - let network = params.instance_parameters.network; - let operator_pubkey = params.operator_pubkey; - let operator_taproot_public_key = XOnlyPublicKey::from(operator_pubkey); - let n_of_n_taproot_public_key = - XOnlyPublicKey::from(params.instance_parameters.committee_agg_pubkey); - let watchtower_num = params.watchtower_pubkeys.len(); - let assert_wots_pubkeys = (params.operator_wots_pubkeys.1, *params.operator_wots_pubkeys.2); - let assert_commit_connectors = generate_chunked_assert_commit_connectors( - network, - &n_of_n_taproot_public_key, - assert_wots_pubkeys, - ); - let assert_commit_num = assert_commit_connectors.len(); - - // pegin - let (_, pegin, _) = params.instance_parameters.build_pegin_tx()?; - let pegin_txid = pegin.tx().compute_txid(); - let connector_0_input = Input { - outpoint: OutPoint { txid: pegin_txid, vout: 0 }, - amount: pegin.tx().output[0].value, - }; - - // prekickoff - let cur_prekickoff_connector = PrekickoffConnector::new(network, &operator_taproot_public_key); - let next_force_skip_connector = ForceSkipConnector::new(network, &operator_taproot_public_key); - let next_kickoff_connector = KickoffConnector::new(network, &operator_taproot_public_key); - let next_prekickoff_connector = PrekickoffConnector::new(network, &operator_taproot_public_key); - let cur_prekickoff = params.prekickoff_parameters.cur_prekickoff_txn.clone(); - let cur_prekickoff_txid = cur_prekickoff.tx().compute_txid(); - let cur_prekickoff_connector_input = Input { - outpoint: OutPoint { txid: cur_prekickoff_txid, vout: 2 }, - amount: cur_prekickoff.tx().output[2].value, - }; - 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, - assert_commit_num, - ) - .map_err(|e| anyhow::anyhow!("failed to create pre-kickoff txn: {e}"))?; - let next_prekickoff_txid = next_prekickoff.tx().compute_txid(); - let next_force_skip_connector_input = Input { - outpoint: OutPoint { txid: next_prekickoff_txid, vout: 0 }, - amount: cur_prekickoff.tx().output[0].value, - }; - let next_prekickoff_connector_input = Input { - outpoint: OutPoint { txid: next_prekickoff_txid, vout: 2 }, - amount: cur_prekickoff.tx().output[2].value, - }; - - // kickoff - let kickoff_connector_input = Input { - outpoint: OutPoint { txid: cur_prekickoff_txid, vout: 1 }, - amount: cur_prekickoff.tx().output[1].value, - }; - let kickoff_connector = KickoffConnector::new(network, &operator_taproot_public_key); - let connector_a = - ConnectorA::new(network, &operator_taproot_public_key, &n_of_n_taproot_public_key); - let connector_b = ConnectorB::new(network, &operator_taproot_public_key); - let connector_c = ConnectorC::new(network, &operator_taproot_public_key); - let guardian_connector = GuardianConnector::new(network, &operator_taproot_public_key); - let kickoff = KickoffTransaction::new_for_validation( - &kickoff_connector, - &connector_a, - &connector_b, - &connector_c, - &connector_e, - &guardian_connector, - &kickoff_connector_input, - watchtower_num, - assert_commit_num, - ) - .map_err(|e| anyhow::anyhow!("failed to create kickoff txn: {e}"))?; - let kickoff_txid = kickoff.tx().compute_txid(); - let connector_a_input = Input { - outpoint: OutPoint { txid: kickoff_txid, vout: 0 }, - amount: kickoff.tx().output[0].value, - }; - let connector_b_input = Input { - outpoint: OutPoint { txid: kickoff_txid, vout: 1 }, - amount: kickoff.tx().output[1].value, - }; - let connector_c_input = Input { - outpoint: OutPoint { txid: kickoff_txid, vout: 2 }, - amount: kickoff.tx().output[2].value, - }; - let connector_e_input = Input { - outpoint: OutPoint { txid: kickoff_txid, vout: 3 }, - amount: kickoff.tx().output[3].value, - }; - let guardian_connector_input = Input { - outpoint: OutPoint { txid: kickoff_txid, vout: 4 }, - amount: kickoff.tx().output[4].value, - }; - - // 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 = Connector0::new(network, &n_of_n_taproot_public_key); - 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-init - let connector_g = ConnectorG::new( - network, - &n_of_n_taproot_public_key, - &operator_taproot_public_key, - ¶ms.operator_wots_pubkeys.0[0], - ); - let connector_f = - ConnectorF::new(network, &operator_taproot_public_key, &n_of_n_taproot_public_key); - let watchtower_connectors_array = (0..watchtower_num) - .map(|i| { - ( - WatchtowerChallengeConnector::new( - network, - &operator_taproot_public_key, - ¶ms.watchtower_pubkeys[i], - ), - AckConnector::new(network, &n_of_n_taproot_public_key, ¶ms.hashlocks[i]), - ) - }) - .collect::>(); - let watchtower_challenge_init = WatchtowerChallengeInitTransaction::new_for_validation( - &connector_b, - &connector_g, - &connector_f, - &watchtower_connectors_array, - connector_b_input, - ) - .map_err(|e| anyhow::anyhow!("failed to create watchtower-challenge-init txn: {e}"))?; - let watchtower_challenge_init_txid = watchtower_challenge_init.tx().compute_txid(); - let connector_g_vout = watchtower_num * 2; - let connector_g_input = Input { - outpoint: OutPoint { txid: watchtower_challenge_init_txid, vout: connector_g_vout as u32 }, - amount: watchtower_challenge_init.tx().output[connector_g_vout].value, - }; - let connector_f_vout = connector_g_vout + 1; - let connector_f_input = Input { - outpoint: OutPoint { txid: watchtower_challenge_init_txid, vout: connector_f_vout as u32 }, - amount: watchtower_challenge_init.tx().output[connector_f_vout].value, - }; - - // watchtower-challenge-timeout & nack - let mut watchtower_challenge_timeout_txns = vec![]; - let mut nack_txns = vec![]; - for (i, watchtower_connectors) in watchtower_connectors_array.iter().enumerate() { - let watchtower_challenge_connector_input_vout: usize = i * 2; - let watchtower_challenge_connector_input = Input { - outpoint: OutPoint { - txid: watchtower_challenge_init_txid, - vout: watchtower_challenge_connector_input_vout as u32, - }, - amount: watchtower_challenge_init.tx().output - [watchtower_challenge_connector_input_vout] - .value, - }; - let ack_connector_input_vout: usize = i * 2 + 1; - let ack_connector_input = Input { - outpoint: OutPoint { - txid: watchtower_challenge_init_txid, - vout: ack_connector_input_vout as u32, - }, - amount: watchtower_challenge_init.tx().output[ack_connector_input_vout].value, - }; - let watchtower_challenge_timeout_tx = - WatchtowerChallengeTimeoutTransaction::new_for_validation( - watchtower_connectors, - watchtower_challenge_connector_input, - ack_connector_input.clone(), - ); - let nack_tx = NackTransaction::new_for_validation( - watchtower_connectors, - &connector_f, - ack_connector_input, - connector_f_input.clone(), - ); - watchtower_challenge_timeout_txns.push(watchtower_challenge_timeout_tx); - nack_txns.push(nack_tx); - } - - // operator-commit-blockhash-timeout - let blockhash_commit_timeout = BlockhashCommitTimeoutTransaction::new_for_validation( - &connector_g, - &connector_f, - connector_g_input.clone(), - connector_f_input.clone(), - ); - - // assert-init - let connector_d = - ConnectorD::new(network, &operator_taproot_public_key, &n_of_n_taproot_public_key); - let assert_init = AssertInitTransaction::new_for_validation( - &connector_c, - &connector_d, - &assert_commit_connectors, - &connector_c_input, - ) - .map_err(|e| anyhow::anyhow!("failed to create assert-init txn: {e}"))?; - let assert_init_txid = assert_init.tx().compute_txid(); - let connector_d_vout: usize = assert_commit_connectors.len(); - let connector_d_input = Input { - outpoint: OutPoint { txid: assert_init_txid, vout: connector_d_vout as u32 }, - amount: assert_init.tx().output[connector_d_vout].value, - }; - - // assert-commit-timeout - let mut assert_commit_timeout_txns = vec![]; - for (i, assert_commit_connector) in assert_commit_connectors.iter().enumerate() { - let assert_commit_timeout_input_0_vout: usize = i; - let assert_commit_timeout_input_0 = Input { - outpoint: OutPoint { - txid: assert_init_txid, - vout: assert_commit_timeout_input_0_vout as u32, - }, - amount: assert_init.tx().output[assert_commit_timeout_input_0_vout].value, - }; - let assert_commit_timeout_tx = AssertCommitTimeoutTransaction::new_for_validation( - assert_commit_connector, - &connector_d, - &assert_commit_timeout_input_0, - &connector_d_input, - ); - assert_commit_timeout_txns.push(assert_commit_timeout_tx); - } - - // take-2 - let take2 = Take2Transaction::new_for_validation( - &connector_0, - &connector_d, - &connector_e, - &connector_f, - &guardian_connector, - connector_0_input, - connector_d_input, - connector_e_input, - connector_f_input, - guardian_connector_input, - ¶ms.operator_receive_address, - ) - .map_err(|e| anyhow::anyhow!("failed to create take-2 txn: {e}"))?; - - Ok(Bitvm2Graph { - 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, - take2, - - watchtower_challenge_init, - watchtower_challenge_timeout_txns, - nack_txns, - blockhash_commit_timeout, - - assert_init, - assert_commit_timeout_txns, - - connector_e, - }) -} - -pub fn operator_pre_sign( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, -) -> 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)?; - let network = context.network; - let operator_taproot_public_key = context.operator_taproot_public_key; - - // presign force_skip_kickoff - let kickoff_connector = KickoffConnector::new(network, &operator_taproot_public_key); - let next_force_skip_connector = ForceSkipConnector::new(network, &operator_taproot_public_key); - 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 = GuardianConnector::new(network, &operator_taproot_public_key); - 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 = PrekickoffConnector::new(network, &operator_taproot_public_key); - 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 Bitvm2Graph, - 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(()) -} - -/// remember to sign replensish inputs (if any) after this -pub fn operator_sign_prekickoff_input_0( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, -) -> Result { - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let prev_prekickoff_connector = PrekickoffConnector::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - ); - 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 Bitvm2Graph, - operator_receive_address: Address, - fee_rate: f64, -) -> Result> { - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let kickoff_connector = KickoffConnector::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - ); - let kickoff_connector_input = Input { - outpoint: OutPoint { txid: graph.cur_prekickoff.tx().compute_txid(), vout: 1 }, - amount: graph.cur_prekickoff.tx().output[1].value, - }; - // 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 Bitvm2Graph, -) -> Result { - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let kickoff_connector = KickoffConnector::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - ); - graph.kickoff.sign_input_0(&operator_context, &kickoff_connector); - Ok(graph.kickoff.tx().clone()) -} - -pub fn operator_sign_take1( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, -) -> 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 = ConnectorA::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - &operator_context.n_of_n_taproot_public_key, - ); - let connector_b = - ConnectorB::new(operator_context.network, &operator_context.operator_taproot_public_key); - let connector_c = - ConnectorC::new(operator_context.network, &operator_context.operator_taproot_public_key); - let guardian_connector = GuardianConnector::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - ); - graph.take1.sign_input_1(&operator_context, &connector_a); - graph.take1.sign_input_2(&operator_context, &connector_b); - graph.take1.sign_input_3(&operator_context, &connector_c); - graph.take1.sign_input_4(&operator_context, &guardian_connector); - Ok(graph.take1.tx().clone()) -} - -pub fn operator_sign_take2( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, -) -> 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 = ConnectorD::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - &operator_context.n_of_n_taproot_public_key, - ); - let connector_f = ConnectorF::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - &operator_context.n_of_n_taproot_public_key, - ); - let guardian_connector = GuardianConnector::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - ); - graph.take2.sign_input_1(&operator_context, &connector_d); - graph.take2.sign_input_2(&operator_context, &graph.connector_e); - graph.take2.sign_input_3(&operator_context, &connector_f); - graph.take2.sign_input_4(&operator_context, &guardian_connector); - Ok(graph.take2.tx().clone()) -} - -pub fn operator_sign_watchtower_challenge_init( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, -) -> Result { - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let connector_b = - ConnectorB::new(operator_context.network, &operator_context.operator_taproot_public_key); - 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 Bitvm2Graph, - watchtower_index: usize, -) -> Result { - if !graph.committee_pre_signed() { - bail!("missing pre-signatures from committee".to_string()) - }; - if watchtower_index >= graph.parameters.watchtower_pubkeys.len() { - bail!("invalid watchtower index {watchtower_index}".to_string()) - }; - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let watchtower_challenge_connector = WatchtowerChallengeConnector::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - &graph.parameters.watchtower_pubkeys[watchtower_index], - ); - let ack_connector = AckConnector::new( - operator_context.network, - &operator_context.n_of_n_taproot_public_key, - &graph.parameters.hashlocks[watchtower_index], - ); - let watchtower_connectors = (watchtower_challenge_connector, ack_connector); - graph.watchtower_challenge_timeout_txns[watchtower_index] - .sign_input_0(&operator_context, &watchtower_connectors); - Ok(graph.watchtower_challenge_timeout_txns[watchtower_index].tx().clone()) -} - -pub fn operator_sign_ack( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, - watchtower_index: usize, - preimage: &Vec, -) -> Result<(TxIn, Amount)> { - if hash160(preimage) != graph.parameters.hashlocks[watchtower_index] { - bail!("invalid preimage for watchtower index {watchtower_index}".to_string()) - }; - if watchtower_index >= graph.parameters.watchtower_pubkeys.len() { - bail!("invalid watchtower index {watchtower_index}".to_string()) - }; - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let watchtower_challenge_connector = WatchtowerChallengeConnector::new( - operator_context.network, - &operator_context.operator_taproot_public_key, - &graph.parameters.watchtower_pubkeys[watchtower_index], - ); - let ack_connector = AckConnector::new( - operator_context.network, - &operator_context.n_of_n_taproot_public_key, - &graph.parameters.hashlocks[watchtower_index], - ); - let watchtower_connectors = (watchtower_challenge_connector, ack_connector); - let ack_vout = watchtower_index * 2 + 1; - let ack_input = Input { - outpoint: OutPoint { - txid: graph.watchtower_challenge_init.tx().compute_txid(), - vout: ack_vout as u32, - }, - amount: graph.watchtower_challenge_init.tx().output[ack_vout].value, - }; - match operator_ack(&watchtower_connectors, preimage, ack_input.clone()) { - Ok(txin) => Ok((txin, ack_input.amount)), - Err(e) => bail!("failed to sign ack for watchtower index {watchtower_index}: {e}"), - } -} - -pub fn operator_sign_blockhash_commit( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, - latest_blockhash: &[u8; 32], - wots_secret_key: &WinternitzSecret, -) -> Result<(TxIn, Amount)> { - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let blockhash_wots_pubkey = graph.parameters.operator_wots_pubkeys.0[0]; - if Wots32::generate_public_key(wots_secret_key) != blockhash_wots_pubkey { - bail!("provided WOTS secret key does not match expected public key".to_string()) - }; - let connector_g = ConnectorG::new( - operator_context.network, - &operator_context.n_of_n_taproot_public_key, - &operator_context.operator_taproot_public_key, - &blockhash_wots_pubkey, - ); - let connector_g_vout = 2 * graph.parameters.watchtower_pubkeys.len() as u64; - let connector_g_input = Input { - outpoint: OutPoint { - txid: graph.watchtower_challenge_init.tx().compute_txid(), - vout: connector_g_vout as u32, - }, - amount: graph.watchtower_challenge_init.tx().output[connector_g_vout as usize].value, - }; - match operator_commit_blockhash( - &connector_g, - latest_blockhash, - wots_secret_key, - connector_g_input.clone(), - ) { - Ok(txin) => Ok((txin, connector_g_input.amount)), - Err(e) => bail!("failed to sign blockhash commit: {e}"), - } -} - -pub fn operator_sign_assert_init( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, -) -> Result { - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - let connector_c = - ConnectorC::new(operator_context.network, &operator_context.operator_taproot_public_key); - graph.assert_init.sign_input_0(&operator_context, &connector_c); - Ok(graph.assert_init.tx().clone()) -} - -pub fn operator_sign_assert_commit( - operator_keypair: Keypair, - graph: &mut Bitvm2Graph, - wots_secret_keys: &OperatorWotsSecretKeys, - guest_inputs: [[u8; 32]; NUM_GUEST_PUBS_ASSERT], - proof: Groth16Proof, - groth16_pubin: PublicInputs, - vk: &VerifyingKey, -) -> Result> { - println!("operator_send_assert_commit start operator_sign_assert_commit"); - let operator_context = graph.parameters.get_operator_context(operator_keypair)?; - if !is_valid_wots_secrets(wots_secret_keys, &graph.parameters.operator_wots_pubkeys) { - bail!("provided WOTS secret keys do not match expected public keys".to_string()) - }; - let assert_wots_pubkeys = - (graph.parameters.operator_wots_pubkeys.1, *graph.parameters.operator_wots_pubkeys.2); - let assert_commit_connectors = generate_chunked_assert_commit_connectors( - operator_context.network, - &operator_context.n_of_n_taproot_public_key, - assert_wots_pubkeys, - ); - let mut assert_commit_inputs = vec![]; - for i in 0..assert_commit_connectors.len() { - let vout = i; - let input = Input { - outpoint: OutPoint { txid: graph.assert_init.tx().compute_txid(), vout: vout as u32 }, - amount: graph.assert_init.tx().output[vout].value, - }; - assert_commit_inputs.push(input); - } - let assert_assertions = ( - guest_inputs, - generate_assertions(proof, groth16_pubin, vk) - .map_err(|e| anyhow::anyhow!("failed to generate assertions: {e}"))?, - ); - match operator_commit_proof( - &assert_commit_connectors, - &wots_secret_keys[1..].to_vec(), - &assert_commit_inputs, - &assert_assertions, - ) { - Ok(txins) => Ok(txins - .into_iter() - .enumerate() - .map(|(i, txin)| (txin, assert_commit_inputs[i].amount)) - .collect::>()), - Err(e) => bail!("failed to sign assert commit: {e}"), - } -} - -pub fn is_valid_wots_secrets( - wots_seckeys: &OperatorWotsSecretKeys, - expected_pubkeys: &OperatorWotsPublicKeys, -) -> bool { - // let generated_pubkeys = wots_secrets_to_pubkeys(wots_seckeys); - // &generated_pubkeys == expected_pubkeys - - // Compare each generated WOTS public key against the expected public keys - // without allocating large temporaries. This keeps stack usage small. - let (guest_extra_expected, guest_assert_expected, proof_pubkeys_box) = expected_pubkeys; - let proof_pubkeys = proof_pubkeys_box.as_ref(); - - let mut idx: usize = 0; - - // guest_extra (Wots32) - for expected in guest_extra_expected.iter() { - let generated = Wots32::generate_public_key(&wots_seckeys[idx]); - if &generated != expected { - return false; - } - idx += 1; - } - - // guest_assert (Wots32) - for expected in guest_assert_expected.iter() { - let generated = Wots32::generate_public_key(&wots_seckeys[idx]); - if &generated != expected { - return false; - } - idx += 1; - } - - // proof pubins (Wots32) - for expected in proof_pubkeys.0.iter() { - let generated = Wots32::generate_public_key(&wots_seckeys[idx]); - if &generated != expected { - return false; - } - idx += 1; - } - - // proof fq_arr (Wots32) - for expected in proof_pubkeys.1.iter() { - let generated = Wots32::generate_public_key(&wots_seckeys[idx]); - if &generated != expected { - return false; - } - idx += 1; - } - - // proof h_arr (Wots16) - for expected in proof_pubkeys.2.iter() { - let generated = Wots16::generate_public_key(&wots_seckeys[idx]); - if &generated != expected { - return false; - } - idx += 1; - } - - true -} - -pub fn take1_timelock(network: Network) -> u32 { - num_blocks_per_network(network, CONNECTOR_A_TIMELOCK) -} - -/// take2 has two timelocks, relative to (watchtower_challenge_init, assert_init) -pub fn take2_timelocks(network: Network) -> (u32, u32) { - ( - num_blocks_per_network(network, CONNECTOR_F_TIMELOCK) - + if network == Network::Testnet { 24 } else { 0 } // Testnet extra delay - + if network == Network::Testnet4 { 48 } else { 0 } // Testnet4 extra delay - + if network == Network::Regtest { 6 } else { 0 }, // Regtest extra delay - num_blocks_per_network(network, CONNECTOR_D_TIMELOCK) - + if network == Network::Testnet { 6 } else { 0 } // Testnet extra delay - + if network == Network::Testnet4 { 100 } else { 0 } // Testnet4 extra delay - + if network == Network::Regtest { 6 } else { 0 }, // Regtest extra delay - ) -} - -pub fn watchtower_challenge_timeout_timelock(network: Network) -> u32 { - num_blocks_per_network(network, WATCHTOWER_CHALLENGE_TIMELOCK) - + if network == Network::Testnet { 12 } else { 0 } // Testnet extra delay - + if network == Network::Testnet4 { 20 } else { 0 } // Testnet4 extra delay - + if network == Network::Regtest { 2 } else { 0 } // Regtest extra delay -} diff --git a/crates/bitvm2-ga/src/tests.rs b/crates/bitvm2-ga/src/tests.rs deleted file mode 100644 index 0d5867a8..00000000 --- a/crates/bitvm2-ga/src/tests.rs +++ /dev/null @@ -1,1497 +0,0 @@ -#![allow(dead_code)] - -#[cfg(test)] -#[cfg(feature = "ci-tests")] -mod tests { - use crate::{challenger::*, committee::*, keys::*, operator::*, types::*, watchtower::*}; - use bitcoin::{ - Address, Amount, EcdsaSighashType, Network, OutPoint, PublicKey, ScriptBuf, TapSighashType, - Transaction, TxIn, TxOut, Txid, XOnlyPublicKey, hashes::Hash, key::Keypair, - }; - use bitcoincore_rpc::{Auth, Client as BtcdClient, RpcApi}; - use bitvm::{ - chunk::api::{NUM_HASH, NUM_PUBS, NUM_U256}, - treepp::*, - }; - use core::panic; - use esplora_client::{AsyncClient as EsploraClient, Builder}; - use goat::{ - connectors::{ - assert_connectors::chunk_assert_commit, - base::TaprootConnector, - connector_z::ConnectorZ, - kickoff_connectors::{ForceSkipConnector, KickoffConnector, PrekickoffConnector}, - }, - constants::CONNECTOR_Z_TIMELOCK, - contexts::{base::generate_n_of_n_public_key, operator::OperatorContext}, - disprove_scripts::{NUM_GUEST_PUBS_ASSERT, hash160}, - scripts::generate_opreturn_script, - transactions::{ - base::{BaseTransaction, DUST_AMOUNT, Input}, - pre_signed::{PreSignedTransaction, pre_sign_taproot_input_default}, - prekickoff::PrekickoffTransaction, - signing::populate_p2wsh_witness, - }, - utils::num_blocks_per_network, - }; - use musig2::PubNonce; - use secp256k1::SECP256K1; - use sha2::{Digest, Sha256}; - use std::path::PathBuf; - use std::{time::Duration, vec}; - use tokio::time::sleep; - use uuid::Uuid; - - fn network() -> Network { - const ENV_KEY: &str = "BITVM_NETWORK"; - match std::env::var(ENV_KEY) { - Ok(v) => match v.to_lowercase().as_str() { - "regtest" => Network::Regtest, - "testnet" => Network::Testnet, - "testnet4" => Network::Testnet4, - "signet" => Network::Signet, - "bitcoin" | "mainnet" => Network::Bitcoin, - _ => Network::Regtest, - }, - Err(_) => Network::Regtest, - } - } - - fn set_network(n: Network) { - const ENV_KEY: &str = "BITVM_NETWORK"; - let v = match n { - Network::Regtest => "regtest", - Network::Testnet => "testnet", - Network::Testnet4 => "testnet4", - Network::Signet => "signet", - Network::Bitcoin => "bitcoin", - }; - unsafe { - std::env::set_var(ENV_KEY, v); - } - } - - fn fee_rate() -> f64 { - 1.0 // sat/vbyte - } - - async fn get_esplora_client() -> EsploraClient { - match network() { - Network::Regtest => Builder::new("http://127.0.0.1:3002").build_async().unwrap(), - Network::Testnet => { - Builder::new("https://mempool.space/testnet/api").build_async().unwrap() - } - _ => panic!("Mainnet is not supported in tests"), - } - } - - fn get_btcd_client() -> BtcdClient { - if network() != Network::Regtest { - panic!("get_btcd_client only supports Regtest"); - } - BtcdClient::new( - "http://127.0.0.1:18443", - Auth::UserPass("111111".to_string(), "111111".to_string()), - ) - .unwrap() - } - - fn gen_keypair(secret: &str) -> Keypair { - if !secret.starts_with("seed:") { - return Keypair::from_seckey_str_global(secret).unwrap(); - } - // derive private key from seed - let hashed = Sha256::digest(secret.as_bytes()); - let sk = secp256k1::SecretKey::from_slice(&hashed).unwrap(); - Keypair::from_secret_key(SECP256K1, &sk) - } - - fn bank_keypair() -> Keypair { - gen_keypair("seed:faucet") - } - - async fn fund_address( - client: &EsploraClient, - network: Network, - address: Address, - amount: Amount, - ) -> OutPoint { - // In regtest, we can use btcd rpc to fund address directly - if network == Network::Regtest { - let fund_txid = get_btcd_client() - .send_to_address(&address, amount, None, None, None, None, None, None) - .unwrap(); - wait_tx_confirm(client, fund_txid).await; - let fund_tx = client.get_tx(&fund_txid).await.unwrap().unwrap(); - let vout = fund_tx - .output - .iter() - .position(|o| o.script_pubkey == address.script_pubkey()) - .unwrap() as u32; - return OutPoint { txid: fund_txid, vout }; - } - - fn estimate_fee(num_inputs: usize, fee_rate: f64) -> Amount { - let tx_size = (num_inputs * 148 + 2 * 34 + 10) as f64; - let fee = (tx_size * fee_rate).ceil() as u64; - Amount::from_sat(fee) - } - let mut fund_tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], - output: vec![bitcoin::TxOut { value: amount, script_pubkey: address.script_pubkey() }], - }; - let bank_pubkey = bank_keypair().public_key().into(); - let bank_address = node_p2wsh_address(network, &bank_pubkey); - let utxos = client.get_address_utxo(bank_address.clone()).await.unwrap(); - let mut sorted_utxos = utxos; - sorted_utxos.sort_by(|a, b| b.value.cmp(&a.value)); - let mut selected = Vec::new(); - let mut total_value = Amount::ZERO; - - for utxo in sorted_utxos.into_iter() { - selected.push(utxo.clone()); - total_value += utxo.value; - fund_tx.input.push(bitcoin::TxIn { - previous_output: OutPoint { txid: utxo.txid, vout: utxo.vout }, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }); - if total_value >= amount + estimate_fee(fund_tx.input.len(), fee_rate()) { - break; - } - } - - let change = total_value - amount - estimate_fee(fund_tx.input.len(), fee_rate()); - if change.to_sat() > DUST_AMOUNT { - fund_tx.output.push(bitcoin::TxOut { - value: change, - script_pubkey: bank_address.script_pubkey(), - }); - } - - let script = node_p2wsh_script(&bank_pubkey); - (0..fund_tx.input.len()).for_each(|index| { - let amount = selected[index].value; - populate_p2wsh_witness( - &mut fund_tx, - index, - EcdsaSighashType::All, - &script, - amount, - &vec![&bank_keypair()], - ); - }); - - client.broadcast(&fund_tx).await.unwrap(); - wait_tx_confirm(client, fund_tx.compute_txid()).await; - OutPoint { txid: fund_tx.compute_txid(), vout: 0 } - } - - async fn fund_address_batch( - client: &EsploraClient, - network: Network, - receivers: Vec<(Address, Amount)>, - ) -> Vec { - fn estimate_fee(num_inputs: usize, num_outputs: usize, fee_rate: f64) -> Amount { - let tx_size = (num_inputs * 148 + (num_outputs + 1) * 34 + 10) as f64; - let fee = (tx_size * fee_rate).ceil() as u64; - Amount::from_sat(fee) - } - let mut fund_tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], - output: vec![], - }; - let output_num = receivers.len(); - let total_output_amount: Amount = receivers.iter().map(|(_, amt)| *amt).sum(); - for (address, amount) in receivers { - fund_tx - .output - .push(bitcoin::TxOut { value: amount, script_pubkey: address.script_pubkey() }); - } - let bank_pubkey = bank_keypair().public_key().into(); - let bank_address = node_p2wsh_address(network, &bank_pubkey); - let utxos = client.get_address_utxo(bank_address.clone()).await.unwrap(); - let mut sorted_utxos = utxos; - sorted_utxos.sort_by(|a, b| b.value.cmp(&a.value)); - let mut selected = Vec::new(); - let mut total_value = Amount::ZERO; - - for utxo in sorted_utxos.into_iter() { - selected.push(utxo.clone()); - total_value += utxo.value; - fund_tx.input.push(bitcoin::TxIn { - previous_output: OutPoint { txid: utxo.txid, vout: utxo.vout }, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }); - if total_value - >= total_output_amount + estimate_fee(fund_tx.input.len(), output_num, fee_rate()) - { - break; - } - } - - let change = total_value - - total_output_amount - - estimate_fee(fund_tx.input.len(), output_num, fee_rate()); - if change.to_sat() > DUST_AMOUNT { - fund_tx.output.push(bitcoin::TxOut { - value: change, - script_pubkey: bank_address.script_pubkey(), - }); - } - - let script = node_p2wsh_script(&bank_pubkey); - (0..fund_tx.input.len()).for_each(|index| { - let amount = selected[index].value; - populate_p2wsh_witness( - &mut fund_tx, - index, - EcdsaSighashType::All, - &script, - amount, - &vec![&bank_keypair()], - ); - }); - - client.broadcast(&fund_tx).await.unwrap(); - wait_tx_confirm(client, fund_tx.compute_txid()).await; - let fund_txid = fund_tx.compute_txid(); - (0..output_num).map(|i| OutPoint { txid: fund_txid, vout: i as u32 }).collect() - } - - async fn find_utxo( - client: &EsploraClient, - address: Address, - min_amount: Amount, - ) -> Option { - let utxos = client.get_address_utxo(address).await.unwrap(); - for utxo in utxos { - if utxo.value >= min_amount { - return Some(Input { - outpoint: OutPoint { txid: utxo.txid, vout: utxo.vout }, - amount: utxo.value, - }); - } - } - None - } - - fn user_master_key() -> Keypair { - gen_keypair("seed:user") - } - - fn operator_master_key() -> OperatorMasterKey { - OperatorMasterKey::new(gen_keypair("seed:operator")) - } - - fn challenger_master_key() -> ChallengerMasterKey { - ChallengerMasterKey::new(gen_keypair("seed:challenger")) - } - - fn committee_member_master_key() -> [CommitteeMasterKey; 3] { - [ - CommitteeMasterKey::new(gen_keypair("seed:committee-1")), - CommitteeMasterKey::new(gen_keypair("seed:committee-2")), - CommitteeMasterKey::new(gen_keypair("seed:committee-3")), - ] - } - - fn committee_instance_keys_envelope_path(instance_id: Uuid, committee_index: usize) -> PathBuf { - let mut path = std::env::temp_dir(); - path.push("bitvm2-ga-committee-instance-keys"); - path.push(format!("{instance_id}-{committee_index}.json")); - path - } - - fn is_io_not_found_error(err: &anyhow::Error) -> bool { - err.chain().any(|cause| { - cause - .downcast_ref::() - .is_some_and(|io_err| io_err.kind() == std::io::ErrorKind::NotFound) - }) - } - - fn load_committee_instance_keypair( - committee_master_key: &CommitteeMasterKey, - instance_id: Uuid, - committee_index: usize, - ) -> Keypair { - let envelope_path = committee_instance_keys_envelope_path(instance_id, committee_index); - committee_master_key.load_instance_keypair(instance_id, &envelope_path).unwrap_or_else( - |err| { - panic!( - "load committee instance keypair failed for {instance_id} at {}: {err}", - envelope_path.display() - ) - }, - ) - } - - fn create_committee_instance_keypair_envelopes(instance_id: Uuid) { - let committee_master_keys = committee_member_master_key(); - for (index, committee_master_key) in committee_master_keys.iter().enumerate() { - let envelope_path = committee_instance_keys_envelope_path(instance_id, index); - match committee_master_key.load_instance_keypair(instance_id, &envelope_path) { - Ok(_) => {} - Err(load_err) => { - if !is_io_not_found_error(&load_err) { - panic!( - "load committee instance keypair failed for {instance_id} at {}: {load_err}", - envelope_path.display() - ); - } - committee_master_key - .create_instance_keypair_envelope(instance_id, &envelope_path) - .unwrap_or_else(|err| { - panic!( - "create committee instance key envelope failed for {instance_id} at {}: {err}", - envelope_path.display() - ) - }); - } - } - } - } - - fn watchtower_master_key() -> [WatchtowerMasterKey; 3] { - [ - WatchtowerMasterKey::new(gen_keypair("seed:watchtower-1")), - WatchtowerMasterKey::new(gen_keypair("seed:watchtower-2")), - WatchtowerMasterKey::new(gen_keypair("seed:watchtower-3")), - ] - } - - fn hashlocks() -> ([Vec; 3], [[u8; 20]; 3]) { - let preimages = [vec![0x11u8; 20], vec![0x22u8; 20], vec![0x33u8; 20]]; - let mut hashlocks = [[0u8; 20]; 3]; - for (i, preimage) in preimages.iter().enumerate() { - let hash = hash160(preimage); - hashlocks[i] = hash; - } - (preimages, hashlocks) - } - - pub fn node_p2wsh_script(pubkey: &PublicKey) -> ScriptBuf { - script! { - { *pubkey } - OP_CHECKSIG - } - .compile() - } - pub fn node_p2wsh_address(network: Network, pubkey: &PublicKey) -> Address { - Address::p2wsh(&node_p2wsh_script(pubkey), network) - } - pub fn node_sign( - tx: &mut Transaction, - input_index: usize, - input_value: Amount, - sighash_type: EcdsaSighashType, - node_keypair: &Keypair, - ) -> Result<(), Box> { - let node_pubkey = node_keypair.public_key(); - populate_p2wsh_witness( - tx, - input_index, - sighash_type, - &node_p2wsh_script(&node_pubkey.into()), - input_value, - &vec![node_keypair], - ); - Ok(()) - } - - fn assert_commit_num() -> usize { - chunk_assert_commit(NUM_GUEST_PUBS_ASSERT + NUM_PUBS + NUM_U256, NUM_HASH, false).len() - } - - fn get_test_proof() - -> ([[u8; 32]; NUM_GUEST_PUBS_ASSERT], Groth16Proof, PublicInputs, VerifyingKey) { - let proof = hex::decode( - "b6ef2c5aa48a2f599a13bc4d8010e4d0190aeb05ff79e21266aff8dde6353d1756191f0959c787f6dedfc0c47751aed2648775101285b9da2d6c4e912e74891f884bd672f94f4d78528fb10b5410a94b53bcef07f99952ef72b68c72a5c4ff2a3de7c314ffbf17df018a753f070448c2f698706d4c2b99bdb06f928cffe1bea0", - ).unwrap(); - let pis = hex::decode( - "02000000000000002000000000000000721db33a295a3b29a61c7360486e6d8346288822dc5cab652722e34d4b423d002000000000000000cfdc2f035c3699c6d17563570ea05a3d6d08302487937dd079a6b1671d484c0d", - ).unwrap(); - let vk = hex::decode( - "e2f26dbea299f5223b646cb1fb33eadb059d9407559d7441dfd902e3a79a4d2dabb73dc17fbc13021e2471e0c08bd67d8401f52b73d6d07483794cad4778180e0c06f33bbc4c79a9cadef253a68084d382f17788f885c9afd176f7cb2f036789edf692d95cbdde46ddda5ef7d422436779445c5e66006a42761e1f12efde0018c212f3aeb785e49712e7a9353349aaf1255dfb31b7bf60723a480d9293938e19ffdb10cf9f7e2b08673477187c33a695a397702cf22005900724518b57f92f2ce08f8dfe36ca3eff63b1743d64812936d8cab0d74c063d260e20a9a3339b2a8c0300000000000000d17e1efc51d15eef04bde8dc794edc9e5788eb7539171d3a49d970ab9215b89c9ab6c5ab119ca81927393ef29332a1d15ac5f197b878ea89a1f8f686b747011eaad636dcb52cdfd674d155ddd67d21186fbdd1c0a62ebd74dcd6ddc6784b819e", - ).unwrap(); - let proof = goat::proof::deserialize_proof(proof); - let pis = goat::proof::deserialize_pubin(pis); - let vk = goat::proof::deserialize_vk(vk); - let guest_pubs = [[0xddu8; 32]; NUM_GUEST_PUBS_ASSERT]; - (guest_pubs, proof, pis, vk) - } - - async fn gen_test_graph( - esplora: &EsploraClient, - disprove_scripts: Vec, - ) -> Bitvm2Graph { - let instance_id = Uuid::new_v4(); - let graph_id = Uuid::new_v4(); - let pegin_amount = Amount::from_sat(10000); - let challenge_amount = Amount::from_sat(10000); - let default_fee_amount = Amount::from_sat(1000); - let bank_address = node_p2wsh_address(network(), &bank_keypair().public_key().into()); - let user_xonly_pubkey = XOnlyPublicKey::from(user_master_key().public_key()); - let user_address = node_p2wsh_address(network(), &user_master_key().public_key().into()); - let pegin_deposit_input_amount = pegin_amount + default_fee_amount * 2; - let pegin_deposit_inputs = vec![Input { - outpoint: fund_address( - esplora, - network(), - user_address.clone(), - pegin_deposit_input_amount, - ) - .await, - amount: pegin_deposit_input_amount, - }]; - let user_info = UserInfo { - depositor_evm_address: [0x11u8; 20], - txn_fees: [default_fee_amount.to_sat(); 3], - inputs: pegin_deposit_inputs, - user_xonly_pubkey, - user_change_address: bank_address.clone(), - user_refund_address: bank_address.clone(), - }; - - // Initialize committee instance envelopes once at flow start. - create_committee_instance_keypair_envelopes(instance_id); - - let committee_pubkeys: Vec = committee_member_master_key() - .iter() - .enumerate() - .map(|(index, k)| { - load_committee_instance_keypair(k, instance_id, index).public_key().into() - }) - .collect(); - let committee_agg_pubkey = generate_n_of_n_public_key(&committee_pubkeys).0; - let instance_params = Bitvm2InstanceParameters { - network: network(), - instance_id, - user_info, - pegin_amount, - committee_pubkeys, - committee_agg_pubkey, - }; - - let mut pegin_deposit_tx = instance_params.build_pegin_tx().unwrap().0; - node_sign( - pegin_deposit_tx.tx_mut(), - 0, - pegin_deposit_input_amount, - EcdsaSighashType::All, - &user_master_key(), - ) - .unwrap(); - println!("broadcasting pegin deposit tx {}", pegin_deposit_tx.tx().compute_txid()); - esplora.broadcast(&pegin_deposit_tx.finalize()).await.unwrap(); - wait_tx_confirm(esplora, pegin_deposit_tx.tx().compute_txid()).await; - println!("pegin deposit tx confirmed"); - - let operator_master_key = operator_master_key(); - let operator_keypair = operator_master_key.master_keypair(); - let operator_wots_pubkeys = operator_master_key.wots_keypair_for_graph(graph_id).1; - let operator_taproot_public_key = XOnlyPublicKey::from(operator_keypair.public_key()); - let prekickoff_connector = - PrekickoffConnector::new(network(), &operator_taproot_public_key); - let force_skip_connector = ForceSkipConnector::new(network(), &operator_taproot_public_key); - let kickoff_connector = KickoffConnector::new(network(), &operator_taproot_public_key); - let operator_prekickoff_input_amount = Amount::from_sat(50000); - let cur_prekickoff_connector_input = Input { - outpoint: fund_address( - esplora, - network(), - prekickoff_connector.generate_taproot_address(), - operator_prekickoff_input_amount, - ) - .await, - amount: operator_prekickoff_input_amount, - }; - let mut cur_prekickoff_txn = PrekickoffTransaction::new_for_validation( - &prekickoff_connector, - &force_skip_connector, - &kickoff_connector, - &prekickoff_connector, - cur_prekickoff_connector_input, - vec![], - vec![], - default_fee_amount.to_sat(), - 3, - assert_commit_num(), - ) - .unwrap(); - let base_context = instance_params.get_base_context(); - let operator_context = OperatorContext { - network: network(), - operator_keypair, - operator_public_key: operator_keypair.public_key().into(), - operator_taproot_public_key, - n_of_n_public_key: base_context.n_of_n_public_key, - n_of_n_public_keys: base_context.n_of_n_public_keys, - n_of_n_taproot_public_key: base_context.n_of_n_taproot_public_key, - }; - cur_prekickoff_txn.sign_input_0(&operator_context, &prekickoff_connector); - println!("broadcasting prekickoff tx {}", cur_prekickoff_txn.tx().compute_txid()); - esplora.broadcast(&cur_prekickoff_txn.finalize()).await.unwrap(); - wait_tx_confirm(esplora, cur_prekickoff_txn.tx().compute_txid()).await; - println!("prekickoff tx confirmed"); - let prekickoff_parameters = PrekickoffParameters { - cur_prekickoff_txn, - replenish_fee_inputs: vec![], - replenish_fee_prev_outs: vec![], - fee_amount: default_fee_amount.to_sat(), - }; - - let graph_parameters = Bitvm2GraphParameters { - instance_parameters: instance_params, - prekickoff_parameters, - graph_id, - graph_nonce: 0, - challenge_amount, - operator_pubkey: operator_keypair.public_key().into(), - operator_wots_pubkeys, - operator_receive_address: bank_address.clone(), - watchtower_pubkeys: watchtower_master_key() - .iter() - .map(|k| k.master_keypair().public_key().x_only_public_key().0) - .collect(), - hashlocks: hashlocks().1.to_vec(), - guest_constant_value: [0u8; 32], // all zero for test - }; - - generate_bitvm_graph(graph_parameters, disprove_scripts).unwrap() - } - - fn operator_presign_graph(graph: &mut Bitvm2Graph) { - operator_pre_sign(operator_master_key().master_keypair(), graph).unwrap(); - } - - fn committee_presign_graph(graph: &mut Bitvm2Graph) { - let instance_id = graph.parameters.instance_parameters.instance_id; - let graph_id = graph.parameters.graph_id; - let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let assert_commit_num = assert_commit_num(); - let commitee_master_keys = committee_member_master_key(); - let commitee_pub_nonces: Vec = commitee_master_keys - .iter() - .enumerate() - .map(|(index, k)| { - let committee_keypair = load_committee_instance_keypair(k, instance_id, index); - k.nonces_for_graph_with_keypair( - instance_id, - graph_id, - watchtower_num, - assert_commit_num, - committee_keypair, - ) - .0 - }) - .collect(); - let agg_nonces = nonces_aggregation(&commitee_pub_nonces).unwrap(); - let committee_partial_sigs = commitee_master_keys - .iter() - .enumerate() - .map(|(index, k)| { - let committee_keypair = load_committee_instance_keypair(k, instance_id, index); - let s = k - .nonces_for_graph_with_keypair( - instance_id, - graph_id, - watchtower_num, - assert_commit_num, - committee_keypair, - ) - .1; - committee_pre_sign(committee_keypair, s, agg_nonces.clone(), graph).unwrap() - }) - .collect::>(); - let commitee_agg_sigs = - signature_aggregation(&committee_partial_sigs, &agg_nonces, graph).unwrap(); - push_committee_pre_signatures(graph, &commitee_agg_sigs).unwrap(); - } - - async fn send_pegin_confirm(esplora: &EsploraClient, graph: &Bitvm2Graph) { - let instance_id = graph.parameters.instance_parameters.instance_id; - let commitee_master_keys = committee_member_master_key(); - let commitee_pub_nonces: Vec = commitee_master_keys - .iter() - .enumerate() - .map(|(index, k)| { - let committee_keypair = load_committee_instance_keypair(k, instance_id, index); - k.nonce_for_instance_with_keypair(instance_id, committee_keypair).1 - }) - .collect(); - let agg_nonce = nonce_aggregation(&commitee_pub_nonces); - let committee_musig2_sigs = commitee_master_keys - .iter() - .enumerate() - .map(|(index, k)| { - let committee_keypair = load_committee_instance_keypair(k, instance_id, index); - let (s, _, _) = k.nonce_for_instance_with_keypair(instance_id, committee_keypair); - sign_pegin_confirm(&graph, committee_keypair, s, agg_nonce.clone()).unwrap() - }) - .collect::>(); - let tx = agg_and_push_pegin_confirm_sigs(graph, committee_musig2_sigs, &agg_nonce).unwrap(); - println!("broadcasting pegin confirm tx {}", tx.compute_txid()); - esplora.broadcast(&tx).await.unwrap(); - wait_tx_confirm(esplora, tx.compute_txid()).await; - println!("pegin confirm tx confirmed"); - } - - async fn send_pegin_refund(esplora: &EsploraClient, graph: &Bitvm2Graph) { - let (pegin_deposit, _, mut pegin_refund) = - graph.parameters.instance_parameters.build_pegin_tx().unwrap(); - let pegin_deposit_txid = pegin_deposit.tx().compute_txid(); - let pegin_deposit_height = wait_tx_confirm(esplora, pegin_deposit_txid).await; - let refund_timelock = num_blocks_per_network(network(), CONNECTOR_Z_TIMELOCK); - println!("waiting for pegin refund timelock"); - wait_timelock(esplora, pegin_deposit_height, refund_timelock).await; - let committee_taproot_pubkey = - XOnlyPublicKey::from(graph.parameters.instance_parameters.committee_agg_pubkey); - let connector_z = ConnectorZ::new( - network(), - &committee_taproot_pubkey, - &graph.parameters.instance_parameters.user_info.user_xonly_pubkey, - ); - pre_sign_taproot_input_default( - &mut pegin_refund, - 0, - TapSighashType::All, - connector_z.generate_taproot_spend_info(), - &vec![&user_master_key()], - ); - println!("broadcasting pegin refund tx {}", pegin_refund.tx().compute_txid()); - esplora.broadcast(&pegin_refund.finalize()).await.unwrap(); - wait_tx_confirm(esplora, pegin_refund.tx().compute_txid()).await; - println!("pegin refund tx confirmed"); - } - - async fn wait_timelock(esplora: &EsploraClient, start_height: u32, timelock: u32) { - if network() == Network::Regtest { - // In regtest, we can just mine blocks to satisfy the timelock - let current_height = esplora.get_height().await.unwrap(); - let mint_blocks_num = (start_height + timelock + 1).saturating_sub(current_height); - if mint_blocks_num == 0 { - return; - } - let rpc = get_btcd_client(); - regtest_mint_blocks(&rpc, mint_blocks_num).await; - sleep(Duration::from_secs(1)).await; - } else { - // In testnet, we have to wait for real blocks - let wait_start = std::time::Instant::now(); - let expected_heigth = start_height + timelock + 1; - loop { - let current_height = esplora.get_height().await.unwrap(); - if current_height >= expected_heigth { - break; - } - sleep(Duration::from_secs(5)).await; - let elapsed_secs = wait_start.elapsed().as_secs(); - if elapsed_secs % 60 == 0 { - println!( - "Waited {} mins for timelock, current height {current_height}, expected_height {expected_heigth}", - elapsed_secs / 60 - ); - } - } - } - } - - async fn wait_tx_confirm(esplora: &EsploraClient, txid: Txid) -> u32 { - if network() == Network::Regtest { - regtest_mint_blocks(&get_btcd_client(), 1).await; - let wait_start = std::time::Instant::now(); - loop { - let tx_status = esplora.get_tx_status(&txid).await.unwrap(); - if let Some(height) = tx_status.block_height { - return height; - } - sleep(Duration::from_secs(1)).await; - let elapsed_secs = wait_start.elapsed().as_secs(); - if elapsed_secs > 60 { - panic!( - "Transaction {txid} not confirmed after {} seconds in regtest", - elapsed_secs - ); - } - } - } else { - let wait_start = std::time::Instant::now(); - loop { - let tx_status = esplora.get_tx_status(&txid).await.unwrap(); - if let Some(height) = tx_status.block_height { - return height; - } - sleep(Duration::from_secs(5)).await; - let elapsed_secs = wait_start.elapsed().as_secs(); - if elapsed_secs % 60 == 0 { - println!("Waited {} mins for {txid}", elapsed_secs / 60); - } - } - } - } - - fn check_tx_sig_witness( - tx: &Transaction, - input_index: usize, - pubkey: &XOnlyPublicKey, - sighash: bitcoin::TapSighash, - ) -> bool { - let secp = bitcoin::secp256k1::Secp256k1::verification_only(); - - let wit = match tx.input.get(input_index) { - Some(input) => &input.witness, - None => return false, - }; - - if wit.is_empty() { - return false; - } - - let sig_with_hash = &wit[0]; - - if sig_with_hash.len() != 65 { - return false; - } - - let sig_bytes = &sig_with_hash[..64]; - let _sighash_flag = sig_with_hash[64]; - - let sig = match bitcoin::secp256k1::schnorr::Signature::from_slice(sig_bytes) { - Ok(s) => s, - Err(_) => return false, - }; - - let msg = bitcoin::secp256k1::Message::from(sighash); - - secp.verify_schnorr(&sig, &msg, pubkey).is_ok() - } - - async fn build_sign_and_broadcast_tx( - esplora: &EsploraClient, - node_keypair: Keypair, - txins: Vec, - total_input_amount: Amount, - txouts: Vec, - ) -> Txid { - let txouts = if txouts.is_empty() { - vec![TxOut { value: Amount::ZERO, script_pubkey: generate_opreturn_script(vec![]) }] - } else { - txouts - }; - let mut tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: txins, - output: txouts, - }; - let total_output_amount: Amount = tx.output.iter().map(|o| o.value).sum(); - let fee_amount = Amount::from_sat( - ((tx.weight().to_vbytes_ceil() + 200) as f64 * fee_rate()).ceil() as u64, - ); - let node_address = node_p2wsh_address(network(), &node_keypair.public_key().into()); - let shortfall = Amount::from_sat( - (total_output_amount + fee_amount) - .to_sat() - .saturating_sub(total_input_amount.to_sat() + DUST_AMOUNT) - + DUST_AMOUNT, - ); - let payer_outpoint = - fund_address(esplora, network(), node_address.clone(), shortfall).await; - wait_tx_confirm(esplora, payer_outpoint.txid).await; - tx.input.push(bitcoin::TxIn { - previous_output: payer_outpoint, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }); - let payer_input_index = tx.input.len() - 1; - node_sign(&mut tx, payer_input_index, shortfall, EcdsaSighashType::All, &node_keypair) - .unwrap(); - esplora.broadcast(&tx).await.unwrap(); - tx.compute_txid() - } - - async fn merge_bank_utxo(esplora: &EsploraClient) { - // get_address_utxo may fail if there are too many UTXOs (e.g. 500), so we merge them before its too late - println!("merging bank UTXOs, network: {}", network()); - let bank_address = node_p2wsh_address(network(), &bank_keypair().public_key().into()); - println!("bank_address: {bank_address}"); - let utxos = esplora.get_address_utxo(bank_address.clone()).await.unwrap(); - if utxos.len() < 300 { - println!("bank UTXOs are not too many {}, no need to merge", utxos.len()); - return; - } - let mut selected_utxos = vec![]; - let current_height = esplora.get_height().await.unwrap(); - // coinbase outputs need 100 blocks to mature, so we leave them untouched - for utxo in utxos.into_iter() { - let tx_info = esplora.get_tx_info(&utxo.txid).await.unwrap().unwrap(); - if tx_info.vin[0].txid == Txid::from_slice(&[0u8; 32]).unwrap() - && tx_info.vin[0].vout == u32::MAX - { - // coinbase output - if let Some(height) = tx_info.status.block_height { - if current_height.saturating_sub(height) < 100 { - continue; - } - } else { - continue; - } - } - selected_utxos.push(utxo.clone()); - } - if selected_utxos.len() < 2 { - println!("not enough mature UTXOs to merge"); - return; - } - let txins: Vec = selected_utxos - .iter() - .map(|u| TxIn { - previous_output: OutPoint { txid: u.txid, vout: u.vout }, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }) - .collect(); - let total_input_amount: Amount = selected_utxos.iter().map(|u| u.value).sum(); - let txout = TxOut { - value: total_input_amount - Amount::from_sat(100000), - script_pubkey: bank_address.script_pubkey(), - }; - let mut tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: txins, - output: vec![txout], - }; - for i in 0..tx.input.len() { - node_sign(&mut tx, i, selected_utxos[i].value, EcdsaSighashType::All, &bank_keypair()) - .unwrap(); - } - esplora.broadcast(&tx).await.unwrap(); - wait_tx_confirm(esplora, tx.compute_txid()).await; - println!("bank UTXOs merged"); - } - - async fn regtest_mint_blocks(btcd: &BtcdClient, num: u32) { - if network() != Network::Regtest { - panic!("can only mint blocks in regtest"); - } - let bank_address = node_p2wsh_address(network(), &bank_keypair().public_key().into()); - btcd.generate_to_address(num.into(), &bank_address).unwrap(); - } - - #[tokio::test] - async fn test_take1() { - set_network(Network::Regtest); - let esplora = get_esplora_client().await; - let disprove_scripts = vec![script! {OP_TRUE}.compile()]; // No disprove, use empty vector to simplify - - merge_bank_utxo(&esplora).await; - let mut graph = gen_test_graph(&esplora, disprove_scripts).await; - operator_presign_graph(&mut graph); - committee_presign_graph(&mut graph); - send_pegin_confirm(&esplora, &graph).await; - - // kickoff - let operator_keypair = operator_master_key().master_keypair(); - let kickoff = operator_sign_kickoff(operator_keypair, &mut graph).unwrap(); - println!("broadcasting kickoff tx {}", kickoff.compute_txid()); - esplora.broadcast(&kickoff).await.unwrap(); - let kickoff_height = wait_tx_confirm(&esplora, kickoff.compute_txid()).await; - println!("kickoff tx confirmed"); - - // take1 - println!("waiting for take1 timelock"); - wait_timelock(&esplora, kickoff_height, take1_timelock(network())).await; - let take1 = operator_sign_take1(operator_keypair, &mut graph).unwrap(); - println!("broadcasting take1 tx {}", take1.compute_txid()); - esplora.broadcast(&take1).await.unwrap(); - wait_tx_confirm(&esplora, take1.compute_txid()).await; - println!("take1 tx confirmed"); - } - - #[tokio::test] - async fn test_take2() { - set_network(Network::Regtest); - let esplora = get_esplora_client().await; - let disprove_scripts = vec![script! {OP_TRUE}.compile()]; // No disprove, use empty vector to simplify - - let bank_address = node_p2wsh_address(network(), &bank_keypair().public_key().into()); - let default_fee_amount = Amount::from_sat(1000); - - merge_bank_utxo(&esplora).await; - let mut graph = gen_test_graph(&esplora, disprove_scripts).await; - operator_presign_graph(&mut graph); - committee_presign_graph(&mut graph); - send_pegin_confirm(&esplora, &graph).await; - - // kickoff - let operator_keypair = operator_master_key().master_keypair(); - let kickoff = operator_sign_kickoff(operator_keypair, &mut graph).unwrap(); - println!("broadcasting kickoff tx {}", kickoff.compute_txid()); - esplora.broadcast(&kickoff).await.unwrap(); - wait_tx_confirm(&esplora, kickoff.compute_txid()).await; - println!("kickoff tx confirmed"); - - // challenge - let (mut challenge_tx, _) = export_challenge_tx(&graph).unwrap(); - let challenge_keypair = challenger_master_key().master_keypair(); - challenge_tx.output.push(bitcoin::TxOut { - value: Amount::ZERO, - script_pubkey: generate_opreturn_script(vec![0xffu8; 20]), - }); - println!("broadcasting challenge tx {}", challenge_tx.compute_txid()); - build_sign_and_broadcast_tx( - &esplora, - challenge_keypair, - challenge_tx.input, - kickoff.output[0].value, - challenge_tx.output, - ) - .await; - println!("challenge tx confirmed"); - - // watchtower challenge init - let watchtower_challenge_init = - operator_sign_watchtower_challenge_init(operator_keypair, &mut graph).unwrap(); - let watchtower_challenge_init_txid = watchtower_challenge_init.compute_txid(); - println!("broadcasting watchtower challenge init tx {}", watchtower_challenge_init_txid); - esplora.broadcast(&watchtower_challenge_init).await.unwrap(); - let watchtower_challenge_init_height = - wait_tx_confirm(&esplora, watchtower_challenge_init_txid).await; - println!("watchtower challenge init tx confirmed"); - - // watchtower[0] challenge - let watchtower_0_keypair = watchtower_master_key()[0].master_keypair(); - let watchtower_challenge_payer_amount = Amount::from_sat(30000); - let watchtower_0_challenge_payer_input = Input { - outpoint: fund_address( - &esplora, - network(), - node_p2wsh_address(network(), &watchtower_0_keypair.public_key().into()), - watchtower_challenge_payer_amount, - ) - .await, - amount: watchtower_challenge_payer_amount, - }; - - const PROOF: &[u8] = - include_bytes!("../../../circuits/data/watchtower/output3.bin.proof.bin"); - const PUBLIC_INPUTS: &[u8] = - include_bytes!("../../../circuits/data/watchtower/output3.bin.public_inputs.bin"); - const VK_HASH: &str = - include_str!("../../../circuits/data/watchtower/output3.bin.vk_hash.bin"); - const PROOF_PART_STARK_VK: &[u8] = - include_bytes!("../../../circuits/data/watchtower/output3.bin.proof_part_stark_vk.bin"); - let proof_part_stark_vk = PROOF_PART_STARK_VK.to_vec(); - - let graph_id = *graph.parameters.graph_id.as_bytes(); - //let total_work = 1006120; - //let consensus_commit_block_height = 503043; - let comm = bitcoin_light_client_circuit::build_watchtower_commitment( - &graph_id, - PROOF, - PUBLIC_INPUTS, - VK_HASH, - &proof_part_stark_vk, - ) - .unwrap(); - - let mut watchtower_0_challenge = build_watchtower_challenge_tx( - &graph, - &watchtower_0_keypair, - 0, - &comm, - vec![watchtower_0_challenge_payer_input], - &bank_address, - default_fee_amount, - ) - .unwrap(); - node_sign( - &mut watchtower_0_challenge, - 1, - watchtower_challenge_payer_amount, - EcdsaSighashType::All, - &watchtower_0_keypair, - ) - .unwrap(); - println!( - "broadcasting watchtower 0 challenge tx {}", - watchtower_0_challenge.compute_txid() - ); - esplora.broadcast(&watchtower_0_challenge).await.unwrap(); - wait_tx_confirm(&esplora, watchtower_0_challenge.compute_txid()).await; - println!("watchtower 0 challenge tx confirmed"); - - // operator ack - let (ack_txin, ack_txin_amount) = - operator_sign_ack(operator_keypair, &mut graph, 0, &hashlocks().0[0]).unwrap(); - println!("broadcasting ack tx"); - let ack_txid = build_sign_and_broadcast_tx( - &esplora, - operator_keypair, - vec![ack_txin], - ack_txin_amount, - vec![], - ) - .await; - wait_tx_confirm(&esplora, ack_txid).await; - println!("ack tx {} confirmed", ack_txid); - - // watchtower challenge timeout - println!("waiting for watchtower challenge timeout"); - wait_timelock( - &esplora, - watchtower_challenge_init_height, - watchtower_challenge_timeout_timelock(network()), - ) - .await; - let watchtower_challenge_timeout_tx = - operator_sign_watchtower_challenge_timeout(operator_keypair, &mut graph, 1).unwrap(); - println!( - "broadcasting watchtower challenge timeout tx {}", - watchtower_challenge_timeout_tx.compute_txid() - ); - esplora.broadcast(&watchtower_challenge_timeout_tx).await.unwrap(); - wait_tx_confirm(&esplora, watchtower_challenge_timeout_tx.compute_txid()).await; - println!("watchtower challenge timeout tx confirmed"); - - // operator_commit_blockhash - let wots_secret_keys = - operator_master_key().wots_keypair_for_graph(graph.parameters.graph_id).0; - let blockhash_wots_secret_key = &wots_secret_keys[0]; - let (operator_commit_blockhash_txin, operator_commit_blockhash_txin_amount) = - operator_sign_blockhash_commit( - operator_keypair, - &mut graph, - &[0xeeu8; 32], - blockhash_wots_secret_key, - ) - .unwrap(); - println!("broadcasting operator commit blockhash tx"); - let operator_commit_blockhash_txid = build_sign_and_broadcast_tx( - &esplora, - operator_keypair, - vec![operator_commit_blockhash_txin], - operator_commit_blockhash_txin_amount, - vec![], - ) - .await; - wait_tx_confirm(&esplora, operator_commit_blockhash_txid).await; - println!("operator commit blockhash tx {} confirmed", operator_commit_blockhash_txid); - - // assert-init - let assert_init_tx = operator_sign_assert_init(operator_keypair, &mut graph).unwrap(); - let assert_init_txid = assert_init_tx.compute_txid(); - println!("broadcasting assert-init tx {}", assert_init_txid); - esplora.broadcast(&assert_init_tx).await.unwrap(); - let assert_init_height = wait_tx_confirm(&esplora, assert_init_txid).await; - println!("assert-init tx confirmed"); - - // assert-commit - let (guest_inputs, proof, groth16_pubin, vk) = get_test_proof(); - let assert_commit_txins = operator_sign_assert_commit( - operator_keypair, - &mut graph, - &wots_secret_keys, - guest_inputs, - proof, - groth16_pubin, - &vk, - ) - .unwrap(); - println!("broadcasting assert-commit tx"); - let mut index = 0; - for (txin, amount) in assert_commit_txins { - let assert_commit_txid = - build_sign_and_broadcast_tx(&esplora, operator_keypair, vec![txin], amount, vec![]) - .await; - wait_tx_confirm(&esplora, assert_commit_txid).await; - println!("assert-commit tx-{index} {} confirmed", assert_commit_txid); - index += 1; - } - - // take2 - println!("waiting for take2 timelock"); - wait_timelock(&esplora, watchtower_challenge_init_height, take2_timelocks(network()).0) - .await; - wait_timelock(&esplora, assert_init_height, take2_timelocks(network()).1).await; - let take2 = operator_sign_take2(operator_keypair, &mut graph).unwrap(); - println!("broadcasting take2 tx {}", take2.compute_txid()); - esplora.broadcast(&take2).await.unwrap(); - wait_tx_confirm(&esplora, take2.compute_txid()).await; - println!("take2 tx confirmed"); - } - - #[tokio::test] - async fn test_nack() { - set_network(Network::Regtest); - let esplora = get_esplora_client().await; - let disprove_scripts = vec![script! {OP_TRUE}.compile()]; // No disprove, use empty vector to simplify - - merge_bank_utxo(&esplora).await; - let mut graph = gen_test_graph(&esplora, disprove_scripts).await; - operator_presign_graph(&mut graph); - committee_presign_graph(&mut graph); - send_pegin_refund(&esplora, &graph).await; - - // kickoff - let operator_keypair = operator_master_key().master_keypair(); - let kickoff = operator_sign_kickoff(operator_keypair, &mut graph).unwrap(); - println!("broadcasting kickoff tx {}", kickoff.compute_txid()); - esplora.broadcast(&kickoff).await.unwrap(); - wait_tx_confirm(&esplora, kickoff.compute_txid()).await; - println!("kickoff tx confirmed"); - - // challenge - let (mut challenge_tx, _) = export_challenge_tx(&graph).unwrap(); - let challenge_keypair = challenger_master_key().master_keypair(); - challenge_tx.output.push(bitcoin::TxOut { - value: Amount::ZERO, - script_pubkey: generate_opreturn_script(vec![0xffu8; 20]), - }); - println!("broadcasting challenge tx {}", challenge_tx.compute_txid()); - build_sign_and_broadcast_tx( - &esplora, - challenge_keypair, - challenge_tx.input, - kickoff.output[0].value, - challenge_tx.output, - ) - .await; - println!("challenge tx confirmed"); - - // watchtower challenge init - let watchtower_challenge_init = - operator_sign_watchtower_challenge_init(operator_keypair, &mut graph).unwrap(); - let watchtower_challenge_init_txid = watchtower_challenge_init.compute_txid(); - println!("broadcasting watchtower challenge init tx {}", watchtower_challenge_init_txid); - esplora.broadcast(&watchtower_challenge_init).await.unwrap(); - let watchtower_challenge_init_height = - wait_tx_confirm(&esplora, watchtower_challenge_init_txid).await; - println!("watchtower challenge init tx confirmed"); - - // nack - println!("waiting for nack timelock"); - wait_timelock(&esplora, watchtower_challenge_init_height, nack_timelock(network())).await; - let nack_tx = graph.nack_txns[0].finalize(); - println!("broadcasting nack tx {}", nack_tx.compute_txid()); - esplora.broadcast(&nack_tx).await.unwrap(); - wait_tx_confirm(&esplora, nack_tx.compute_txid()).await; - println!("nack tx confirmed"); - } - - #[tokio::test] - async fn test_commit_timeout() { - set_network(Network::Regtest); - let esplora = get_esplora_client().await; - let disprove_scripts = vec![script! {OP_TRUE}.compile()]; // No disprove, use empty vector to simplify - - merge_bank_utxo(&esplora).await; - let mut graph = gen_test_graph(&esplora, disprove_scripts).await; - operator_presign_graph(&mut graph); - committee_presign_graph(&mut graph); - send_pegin_refund(&esplora, &graph).await; - - // kickoff - let operator_keypair = operator_master_key().master_keypair(); - let kickoff = operator_sign_kickoff(operator_keypair, &mut graph).unwrap(); - println!("broadcasting kickoff tx {}", kickoff.compute_txid()); - esplora.broadcast(&kickoff).await.unwrap(); - wait_tx_confirm(&esplora, kickoff.compute_txid()).await; - println!("kickoff tx confirmed"); - - // challenge - let (mut challenge_tx, _) = export_challenge_tx(&graph).unwrap(); - let challenge_keypair = challenger_master_key().master_keypair(); - challenge_tx.output.push(bitcoin::TxOut { - value: Amount::ZERO, - script_pubkey: generate_opreturn_script(vec![0xffu8; 20]), - }); - println!("broadcasting challenge tx {}", challenge_tx.compute_txid()); - build_sign_and_broadcast_tx( - &esplora, - challenge_keypair, - challenge_tx.input, - kickoff.output[0].value, - challenge_tx.output, - ) - .await; - println!("challenge tx confirmed"); - - // watchtower challenge init - let watchtower_challenge_init = - operator_sign_watchtower_challenge_init(operator_keypair, &mut graph).unwrap(); - let watchtower_challenge_init_txid = watchtower_challenge_init.compute_txid(); - println!("broadcasting watchtower challenge init tx {}", watchtower_challenge_init_txid); - esplora.broadcast(&watchtower_challenge_init).await.unwrap(); - let watchtower_challenge_init_height = - wait_tx_confirm(&esplora, watchtower_challenge_init_txid).await; - println!("watchtower challenge init tx confirmed"); - - // assert-init - let assert_init_tx = operator_sign_assert_init(operator_keypair, &mut graph).unwrap(); - let assert_init_txid = assert_init_tx.compute_txid(); - println!("broadcasting assert-init tx {}", assert_init_txid); - esplora.broadcast(&assert_init_tx).await.unwrap(); - let assert_init_height = wait_tx_confirm(&esplora, assert_init_txid).await; - println!("assert-init tx confirmed"); - - // assert-commit timeout - // normally, only one of assert-commit or blockhash-commit timeout will be triggered - println!("waiting for assert-commit timeout"); - wait_timelock(&esplora, assert_init_height, assert_commit_timeout_timelock(network())) - .await; - let assert_commit_timeout_tx = graph.assert_commit_timeout_txns[0].finalize(); - println!( - "broadcasting assert-commit timeout tx {}", - assert_commit_timeout_tx.compute_txid() - ); - esplora.broadcast(&assert_commit_timeout_tx).await.unwrap(); - wait_tx_confirm(&esplora, assert_commit_timeout_tx.compute_txid()).await; - println!("assert-commit timeout tx confirmed"); - - // blockhash-commit timeout - println!("waiting for blockhash-commit timeout"); - wait_timelock( - &esplora, - watchtower_challenge_init_height, - commit_blockhash_timeout_timelock(network()), - ) - .await; - let blockhash_commit_timeout_tx = graph.blockhash_commit_timeout.finalize(); - println!( - "broadcasting blockhash-commit timeout tx {}", - blockhash_commit_timeout_tx.compute_txid() - ); - esplora.broadcast(&blockhash_commit_timeout_tx).await.unwrap(); - wait_tx_confirm(&esplora, blockhash_commit_timeout_tx.compute_txid()).await; - println!("blockhash-commit timeout tx confirmed"); - } - - #[ignore = "debug"] - #[tokio::test] - async fn test_broadcast_cpfp_package() { - use client::btc_chain::BTCClient; - use goat::scripts::*; - - let network = Network::Testnet; - set_network(network); - let esplora = get_esplora_client().await; - let client = BTCClient::new(network, None); - let test_keypair = gen_keypair("seed:test"); - let test_address = node_p2wsh_address(network, &test_keypair.public_key().into()); - let default_input_amount = Amount::from_sat(2000); - let receivers = std::iter::repeat((test_address.clone(), default_input_amount)) - .take(3) - .collect::>(); - let utxos = fund_address_batch(&esplora, network, receivers).await; - let [utxo0, utxo1, utxo2]: [OutPoint; 3] = utxos.try_into().unwrap(); - let mut txn0 = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![TxIn { - previous_output: utxo0, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }], - output: vec![ - TxOut { value: Amount::ZERO, script_pubkey: test_address.script_pubkey() }, - p2a_output(), - ], - }; - node_sign( - &mut txn0, - 0, - default_input_amount, - bitcoin::EcdsaSighashType::All, - &test_keypair, - ) - .unwrap(); - let output0_amount = - default_input_amount - Amount::from_sat(txn0.weight().to_vbytes_ceil()) - p2a_amount(); - txn0.output[0].value = output0_amount; - txn0.input[0].witness.clear(); - node_sign( - &mut txn0, - 0, - default_input_amount, - bitcoin::EcdsaSighashType::All, - &test_keypair, - ) - .unwrap(); - let txn0_txid = txn0.compute_txid(); - - let mut cpfp_txn0 = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![ - TxIn { - previous_output: OutPoint { txid: txn0_txid, vout: 1 }, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }, - TxIn { - previous_output: utxo1, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }, - ], - output: vec![p2a_output()], - }; - node_sign( - &mut cpfp_txn0, - 1, - default_input_amount, - bitcoin::EcdsaSighashType::All, - &test_keypair, - ) - .unwrap(); - - let mut txn1 = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { txid: txn0_txid, vout: 0 }, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }], - output: vec![ - TxOut { value: Amount::ZERO, script_pubkey: test_address.script_pubkey() }, - p2a_output(), - ], - }; - let input0_amount = txn0.output[0].value; - node_sign(&mut txn1, 0, input0_amount, bitcoin::EcdsaSighashType::All, &test_keypair) - .unwrap(); - let output0_amount = - input0_amount - Amount::from_sat(txn1.weight().to_vbytes_ceil()) - p2a_amount(); - txn1.output[0].value = output0_amount; - txn1.input[0].witness.clear(); - node_sign(&mut txn1, 0, input0_amount, bitcoin::EcdsaSighashType::All, &test_keypair) - .unwrap(); - let txn1_txid = txn1.compute_txid(); - - let mut cpfp_txn1 = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![ - TxIn { - previous_output: OutPoint { txid: txn1_txid, vout: 1 }, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }, - TxIn { - previous_output: utxo2, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }, - ], - output: vec![p2a_output()], - }; - node_sign( - &mut cpfp_txn1, - 1, - default_input_amount, - bitcoin::EcdsaSighashType::All, - &test_keypair, - ) - .unwrap(); - - let tx_package_1 = vec![txn0, cpfp_txn0]; - let tx_package_2 = vec![txn1, cpfp_txn1]; - println!( - "txids in the package: \ntxn0 {}, \ncpfp_txn0 {}, \ntxn1 {}, \ncpfp_txn1 {}", - tx_package_1[0].compute_txid(), - tx_package_1[1].compute_txid(), - tx_package_2[0].compute_txid(), - tx_package_2[1].compute_txid() - ); - client.broadcast_package(&tx_package_1).await.unwrap(); - client.broadcast_package(&tx_package_2).await.unwrap(); - - // this will fail because cpfp_txn0 and cpfp_txn1 not parent & child, only 1c1p is allowed in a package - // let tx_package_1 = vec![txn0, txn1]; - // let tx_package_2 = vec![cpfp_txn0, cpfp_txn1]; - // println!("txids in the package: \ntxn0 {}, \ncpfp_txn0 {}, \ntxn1 {}, \ncpfp_txn1 {}", - // tx_package_1[0].compute_txid(), - // tx_package_2[0].compute_txid(), - // tx_package_1[1].compute_txid(), - // tx_package_2[1].compute_txid()); - // client.broadcast_package(&tx_package_1).await.unwrap(); - // client.broadcast_package(&tx_package_2).await.unwrap(); - - // // this will fail because both cpfp_txn0 and txn1 depends on txn0, only 1c1p is allowed in a package - // let tx_package = vec![txn0, cpfp_txn0, txn1, cpfp_txn1]; - // client.broadcast_package(&tx_package).await.unwrap(); - } - - #[tokio::test] - async fn test_unlimited_opreturn() { - let network = Network::Testnet; - set_network(network); - let esplora = get_esplora_client().await; - let bank_address = node_p2wsh_address(network, &bank_keypair().public_key().into()); - let utxos = esplora.get_address_utxo(bank_address.clone()).await.unwrap(); - // select the largest amount of utxo - let utxo = utxos - .into_iter() - .max_by_key(|utxo| utxo.value) - .expect("No utxo found for bank_address"); - - let msg = b"test OP_RETURN with more than 80 bytes.\n\"A purely peer-to-peer version of electronic cash would allow online payments to be sent directly from one party to another without going through a financial institution. Digital signatures provide part of the solution, but the main benefits are lost if a trusted third party is still required to prevent double-spending.We propose a solution to the double-spending problem using a peer-to-peer network.The network timestamps transactions by hashing them into an ongoing chain of hash-based proof-of-work, forming a record that cannot be changed without redoing the proof-of-work. The longest chain not only serves as proof of the sequence of events witnessed, but proof that it came from the largest pool of CPU power. As long as a majority of CPU power is controlled by nodes that are not cooperating to attack the network, they'll generate the longest chain and outpace attackers. The network itself requires minimal structure. Messages are broadcast on a best effort basis, and nodes can leave and rejoin the network at will, accepting the longest proof-of-work chain as proof of what happened while they were gone.\""; - // let msg = b"short OP_RETURN message"; - let opreturn_script = script! { - OP_RETURN - {msg.to_vec()} - } - .compile(); - let mut tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![TxIn { - previous_output: OutPoint { txid: utxo.txid, vout: utxo.vout }, - script_sig: ScriptBuf::new(), - sequence: bitcoin::Sequence::MAX, - witness: bitcoin::Witness::default(), - }], - output: vec![ - TxOut { value: Amount::ZERO, script_pubkey: opreturn_script }, - TxOut { value: Amount::ZERO, script_pubkey: bank_address.script_pubkey() }, - ], - }; - let tx_size = tx.weight().to_vbytes_ceil() + 200; - if utxo.value < Amount::from_sat(tx_size) { - panic!("cannot find utxo with enough value for the test"); - } - let change_amount = utxo.value - Amount::from_sat(tx_size); // 1 sat/vbyte fee - if change_amount > Amount::from_sat(330) { - tx.output[1].value = change_amount; - } else { - tx.output.pop(); // remove change output if it's too small - } - node_sign(&mut tx, 0, utxo.value, EcdsaSighashType::All, &bank_keypair()).unwrap(); - esplora.broadcast(&tx).await.unwrap(); - println!( - "Broadcasted transaction with {} bytes OP_RETURN: {}", - msg.len(), - tx.compute_txid() - ); - } -} diff --git a/crates/bitvm2-ga/src/types.rs b/crates/bitvm2-ga/src/types.rs deleted file mode 100644 index 5229ab89..00000000 --- a/crates/bitvm2-ga/src/types.rs +++ /dev/null @@ -1,747 +0,0 @@ -use std::collections::BTreeMap; - -use anyhow::{Result, bail}; -use bitcoin::taproot::LeafVersion; -use bitcoin::{Address, Amount, Network, OutPoint, PublicKey, TxOut, XOnlyPublicKey, key::Keypair}; -use bitcoin::{PrivateKey, Witness}; -use bitvm::chunk::api::{ - NUM_HASH, NUM_PUBS, NUM_U256, PublicKeys as ProofWotsPubkeys, - Signatures as Groth16ProofSignatures, -}; -use bitvm::signatures::{WinternitzSecret, Wots, Wots32}; -use goat::connectors::base::TaprootConnector; -use goat::connectors::connector_0::Connector0; -use goat::connectors::connector_e::ConnectorE; -use goat::connectors::connector_z::ConnectorZ; -use goat::contexts::base::BaseContext; -use goat::contexts::operator::OperatorContext; -use goat::contexts::verifier::VerifierContext; -use goat::disprove_scripts::{ - GuestPubinSignatures, NUM_GUEST, NUM_GUEST_PUBS_ASSERT, NUM_GUEST_PUBS_EXTRA, -}; -use goat::transactions::assert::{AssertCommitTimeoutTransaction, AssertInitTransaction}; -use goat::transactions::base::Input; -use goat::transactions::challenge::ChallengeTransaction; -use goat::transactions::kickoff::KickoffTransaction; -use goat::transactions::pegin::{ - PegInConfirmTransaction, PegInDepositTransaction, PegInRefundTransaction, -}; -use goat::transactions::pre_signed::PreSignedTransaction; -use goat::transactions::prekickoff::{ - ChallengeIncompleteKickoffTransaction, ForceSkipKickoffTransaction, PrekickoffTransaction, - QuickChallengeTransaction, -}; -use goat::transactions::take1::Take1Transaction; -use goat::transactions::take2::Take2Transaction; -use goat::transactions::watchtower_challenge::{ - BlockhashCommitTimeoutTransaction, NackTransaction, WatchtowerChallengeInitTransaction, - WatchtowerChallengeTimeoutTransaction, -}; -use rand::{Rng, distributions::Alphanumeric}; -use secp256k1::SECP256K1; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -use crate::committee::{CommitteeSignatures, push_committee_pre_signatures}; -use crate::operator::{generate_bitvm_graph_inner, push_operator_pre_signature}; - -pub type VerifyingKey = ark_groth16::VerifyingKey; -pub type Groth16Proof = ark_groth16::Proof; -pub type PublicInputs = Vec; -pub type GuestInputs = [[u8; 32]; NUM_GUEST_PUBS_ASSERT]; - -pub type OperatorWotsSignatures = (GuestPubinSignatures, Groth16ProofSignatures); - -const NUM_SIGS: usize = NUM_GUEST + NUM_PUBS + NUM_HASH + NUM_U256; -pub type OperatorWotsSecretKeys = Box<[WinternitzSecret; NUM_SIGS]>; - -pub type OperatorWotsPublicKeys = ( - [::PublicKey; NUM_GUEST_PUBS_EXTRA], - [::PublicKey; NUM_GUEST_PUBS_ASSERT], - Box, -); - -pub fn random_string(len: usize) -> String { - rand::thread_rng().sample_iter(&Alphanumeric).take(len).map(char::from).collect() -} - -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone)] -pub struct UserInfo { - pub depositor_evm_address: [u8; 20], - pub txn_fees: [u64; 3], // [ peginDeposit , peginComfirm peginReufnd ] fees in satoshi - 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 Bitvm2InstanceParameters { - 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 Bitvm2GraphParameters { - pub instance_parameters: Bitvm2InstanceParameters, - pub prekickoff_parameters: PrekickoffParameters, - pub graph_id: Uuid, - pub graph_nonce: u64, - pub challenge_amount: Amount, - pub operator_pubkey: PublicKey, - #[serde(with = "node_serializer::wots_pubkeys")] - pub operator_wots_pubkeys: OperatorWotsPublicKeys, - #[serde(with = "node_serializer::address")] - pub operator_receive_address: Address, - pub watchtower_pubkeys: Vec, - pub hashlocks: Vec<[u8; 20]>, // one for each watchtower - pub guest_constant_value: [u8; 32], -} - -impl Bitvm2InstanceParameters { - pub fn check_parameters(&self) -> Result { - // TODO - bail!("Not implemented"); - } - - pub fn build_pegin_tx( - &self, - ) -> Result<(PegInDepositTransaction, PegInConfirmTransaction, PegInRefundTransaction)> { - let network = self.network; - let n_of_n_taproot_public_key = XOnlyPublicKey::from(self.committee_agg_pubkey); - let user_taproot_public_key = self.user_info.user_xonly_pubkey; - let connector_0 = Connector0::new(network, &n_of_n_taproot_public_key); - let connector_z = - ConnectorZ::new(network, &n_of_n_taproot_public_key, &user_taproot_public_key); - 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 = Input { - outpoint: OutPoint { txid: pegin_deposit.tx().compute_txid(), vout: 0 }, - amount: pegin_deposit.tx().output[0].value, - }; - 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 network = self.network; - let n_of_n_taproot_public_key = XOnlyPublicKey::from(self.committee_agg_pubkey); - let user_taproot_public_key = self.user_info.user_xonly_pubkey; - let connector_z = - ConnectorZ::new(network, &n_of_n_taproot_public_key, &user_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 = Input { - outpoint: OutPoint { txid: pegin_deposit.tx().compute_txid(), vout: 0 }, - amount: pegin_deposit.tx().output[0].value, - }; - 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_verifier_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(VerifierContext { - network, - verifier_keypair: committee_member_keypair, - verifier_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 Bitvm2GraphParameters { - 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 Bitvm2Graph { - pub(crate) operator_pre_signed: bool, - pub(crate) committee_pre_signed: bool, - pub parameters: Bitvm2GraphParameters, - - 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 take2: Take2Transaction, - - pub watchtower_challenge_init: WatchtowerChallengeInitTransaction, - pub watchtower_challenge_timeout_txns: Vec, - pub nack_txns: Vec, - pub blockhash_commit_timeout: BlockhashCommitTimeoutTransaction, - - pub assert_init: AssertInitTransaction, - pub assert_commit_timeout_txns: Vec, - - pub connector_e: ConnectorE, -} - -#[derive(Serialize, Deserialize, Clone)] -pub struct SimplifiedBitvm2Graph { - pub(crate) operator_pre_signed: bool, - pub(crate) committee_pre_signed: bool, - pub parameters: Bitvm2GraphParameters, - pub connector_e: ConnectorE, - pub assert_commit_num: usize, - pub operator_pre_sigs: Option>, - pub committee_pre_sigs: Option, -} - -impl Bitvm2Graph { - pub fn operator_pre_signed(&self) -> bool { - self.operator_pre_signed - } - pub fn committee_pre_signed(&self) -> bool { - self.committee_pre_signed - } - 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)?]; - 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 blockhash_commit_timeout = vec![ - extract_sig_from_witness(&self.blockhash_commit_timeout.tx().input[0].witness)?, - extract_sig_from_witness(&self.blockhash_commit_timeout.tx().input[1].witness)?, - ]; - let mut watchtower_challenge_timeout = Vec::new(); - let mut nack = Vec::new(); - for i in 0..self.parameters.watchtower_pubkeys.len() { - let watchtower_challeng_timeout_sig = extract_sig_from_witness( - &self.watchtower_challenge_timeout_txns[i].tx().input[1].witness, - )?; - watchtower_challenge_timeout.push(watchtower_challeng_timeout_sig); - let nack_sig0 = extract_sig_from_witness(&self.nack_txns[i].tx().input[0].witness)?; - let nack_sig1 = extract_sig_from_witness(&self.nack_txns[i].tx().input[1].witness)?; - nack.push(nack_sig0); - nack.push(nack_sig1); - } - let mut assert_commit_timeout = Vec::new(); - for i in 0..self.assert_commit_timeout_txns.len() { - let sig_0 = extract_sig_from_witness( - &self.assert_commit_timeout_txns[i].tx().input[0].witness, - )?; - let sig_1 = extract_sig_from_witness( - &self.assert_commit_timeout_txns[i].tx().input[1].witness, - )?; - assert_commit_timeout.push(sig_0); - assert_commit_timeout.push(sig_1); - } - Some(CommitteeSignatures { - take1, - take2, - challenge, - blockhash_commit_timeout, - watchtower_challenge_timeout, - nack, - assert_commit_timeout, - }) - } else { - None - }; - Ok(SimplifiedBitvm2Graph { - operator_pre_signed: self.operator_pre_signed, - committee_pre_signed: self.committee_pre_signed, - parameters: self.parameters.clone(), - connector_e: self.connector_e.clone(), - assert_commit_num: self.assert_commit_timeout_txns.len(), - operator_pre_sigs, - committee_pre_sigs, - }) - } - pub fn from_simplified(simplified: &SimplifiedBitvm2Graph) -> Result { - let mut graph = generate_bitvm_graph_inner( - simplified.parameters.clone(), - simplified.connector_e.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) - } -} - -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 serde::{self, Deserialize, Deserializer, Serializer}; - use std::str::FromStr; - - pub mod address { - use super::*; - use bitcoin::Address; - - 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)?; - match Address::from_str(&s) { - Ok(addr) => Ok(addr.assume_checked()), - Err(e) => Err(serde::de::Error::custom(e)), - } - } - } - - pub mod wots_pubkeys { - use super::*; - use crate::types::OperatorWotsPublicKeys; - use bitvm::chunk::api::{NUM_HASH, NUM_PUBS, NUM_U256}; - use bitvm::signatures::{Wots, Wots16, Wots32}; - use goat::disprove_scripts::{NUM_GUEST_PUBS_ASSERT, NUM_GUEST_PUBS_EXTRA}; - use serde::de::Error as DeError; - use serde::ser::SerializeSeq; - - pub fn serialize( - pubkeys: &OperatorWotsPublicKeys, - serializer: S, - ) -> Result - where - S: Serializer, - { - let total_len = pubkeys.0.len() - + pubkeys.1.len() - + pubkeys.2.0.len() - + pubkeys.2.1.len() - + pubkeys.2.2.len(); - - let mut seq = serializer.serialize_seq(Some(total_len))?; - - fn push_pk(seq: &mut S, pk: &::PublicKey) -> Result<(), S::Error> - where - S: SerializeSeq, - W: Wots, - { - // pk: AsRef<[[u8; 20]]> - let digits = pk.as_ref(); - - debug_assert_eq!(digits.len(), W::TOTAL_DIGIT_LEN as usize); - - let out: Vec> = digits.iter().map(|d| d.to_vec()).collect(); - seq.serialize_element(&out) - } - - for pk in pubkeys.0.iter() { - push_pk::<_, Wots32>(&mut seq, pk)?; - } - for pk in pubkeys.1.iter() { - push_pk::<_, Wots32>(&mut seq, pk)?; - } - for pk in pubkeys.2.0.iter() { - push_pk::<_, Wots32>(&mut seq, pk)?; - } - for pk in pubkeys.2.1.iter() { - push_pk::<_, Wots32>(&mut seq, pk)?; - } - for pk in pubkeys.2.2.iter() { - push_pk::<_, Wots16>(&mut seq, pk)?; - } - - seq.end() - } - - pub fn deserialize<'de, D>(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let all: Vec>> = Vec::deserialize(deserializer)?; - let expected = - NUM_GUEST_PUBS_EXTRA + NUM_GUEST_PUBS_ASSERT + NUM_PUBS + NUM_U256 + NUM_HASH; - - if all.len() != expected { - return Err(D::Error::custom(format!( - "Invalid WOTS pubkey count: expected {expected}, got {}", - all.len() - ))); - } - - let mut cursor = 0; - fn extract_wots_pubkeys( - src: &[Vec>], - cursor: usize, - label: &str, - ) -> Result<[::PublicKey; N], E> - where - W: Wots, - E: DeError, - { - let digit_len = W::TOTAL_DIGIT_LEN as usize; - if src.len().checked_sub(cursor).is_none_or(|r| r < N) { - return Err(E::custom(format!( - "{label}: not enough elements: need {N}, have {}", - src.len() - cursor - ))); - } - let slice = &src[cursor..cursor + N]; - - let mut out: Vec<::PublicKey> = Vec::with_capacity(N); - for (i, pk) in slice.iter().enumerate() { - if pk.len() != digit_len { - return Err(E::custom(format!( - "{label}[{i}] invalid digit len: expected {digit_len}, got {}", - pk.len() - ))); - } - - let mut digits: Vec<[u8; 20]> = Vec::with_capacity(digit_len); - for (j, d) in pk.iter().enumerate() { - let arr: [u8; 20] = d.as_slice().try_into().map_err(|_| { - E::custom(format!("{label}[{i}][{j}] invalid hash len (expected 20)")) - })?; - digits.push(arr); - } - - let pk: ::PublicKey = digits - .try_into() - .map_err(|_| E::custom(format!("{label}[{i}] size mismatch")))?; - - out.push(pk); - } - - out.try_into().map_err(|_| E::custom(format!("{label}: final size mismatch"))) - } - - let pk0 = extract_wots_pubkeys::( - &all, - cursor, - "guestpk.extra", - )?; - cursor += NUM_GUEST_PUBS_EXTRA; - - let pk1 = extract_wots_pubkeys::( - &all, - cursor, - "guestpk.assert", - )?; - cursor += NUM_GUEST_PUBS_ASSERT; - - let pk20 = - extract_wots_pubkeys::(&all, cursor, "groth16pk.pub")?; - cursor += NUM_PUBS; - - let pk21 = extract_wots_pubkeys::( - &all, - cursor, - "groth16pk.wots256", - )?; - cursor += NUM_U256; - - // FIXME: this is a tricky way to handle Wots16: if we use ? modifier, this will raise SEGV. - #[allow(clippy::question_mark)] - let pk22 = match extract_wots_pubkeys::( - &all, - cursor, - "groth16pk.wots_hash", - ) { - Err(e) => return Err(e), - Ok(pk) => pk, - }; - Ok((pk0, pk1, Box::new((pk20, pk21, pk22)))) - } - } -} - -#[cfg(test)] -mod tests { - use crate::operator::generate_wots_keys; - use crate::types::{OperatorWotsPublicKeys, node_serializer}; - use bitcoin::{Address, Network, key::PublicKey}; - use rand::rngs::OsRng; - use secp256k1::{Keypair, Secp256k1, SecretKey}; - use serde::{Deserialize, Serialize}; - use std::fmt::Debug; - - #[derive(Clone, Copy)] - pub enum AddrKind { - P2pkh, - P2wpkh, - P2shWpkh, - P2tr, - } - - fn random_address(network: Network, kind: AddrKind) -> Address { - let secp = Secp256k1::new(); - - match kind { - AddrKind::P2tr => { - let kp = Keypair::new(&secp, &mut OsRng); - let (xonly, _) = kp.x_only_public_key(); - Address::p2tr(&secp, xonly, None, network) - } - _ => { - let sk = SecretKey::new(&mut OsRng); - let pk = PublicKey::new(secp256k1::PublicKey::from_secret_key(&secp, &sk)); - - let privkey = bitcoin::key::PrivateKey { - compressed: true, - network: network.into(), - inner: sk, - }; - let cpk = bitcoin::CompressedPublicKey::from_private_key(&secp, &privkey).unwrap(); - - match kind { - AddrKind::P2pkh => Address::p2pkh(&pk, network), - AddrKind::P2wpkh => Address::p2wpkh(&cpk, network), - AddrKind::P2shWpkh => Address::p2shwpkh(&cpk, network), - AddrKind::P2tr => unreachable!(), - } - } - } - } - - #[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] - pub struct WotsKeys { - #[serde(with = "node_serializer::wots_pubkeys")] - pub pubs: OperatorWotsPublicKeys, - #[serde(with = "node_serializer::address")] - pub address: Address, - } - - #[cfg(test)] - impl Debug for WotsKeys { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "WotsKeys(..)") - } - } - - #[test] - fn test_wots_keys_serializer() { - for &network in &[Network::Bitcoin, Network::Testnet, Network::Signet, Network::Regtest] { - for kind in &[AddrKind::P2pkh, AddrKind::P2wpkh, AddrKind::P2shWpkh, AddrKind::P2tr] { - let (_, pubs) = generate_wots_keys("seed"); - let address = random_address(network, *kind); - let original = WotsKeys { pubs, address }; - - let json = serde_json::to_vec(&original).unwrap(); - let parsed: WotsKeys = serde_json::from_slice(&json).unwrap(); - assert_eq!(original, parsed); - - let encoded = bincode::serialize(&original).unwrap(); - let decoded: WotsKeys = bincode::deserialize(&encoded).unwrap(); - assert_eq!(original, decoded); - } - } - } - - #[test] - fn test_address_serializer() { - #[derive(Serialize, Deserialize)] - struct AddressTest { - #[serde(with = "node_serializer::address")] - address: Address, - } - for &network in &[Network::Bitcoin, Network::Testnet, Network::Signet, Network::Regtest] { - for kind in &[AddrKind::P2pkh, AddrKind::P2wpkh, AddrKind::P2shWpkh, AddrKind::P2tr] { - let address = random_address(network, *kind); - let test_instance = AddressTest { address: address.clone() }; - let json = serde_json::to_string(&test_instance).unwrap(); - let parsed: AddressTest = serde_json::from_str(&json).unwrap(); - assert_eq!(address, parsed.address); - } - } - } -} diff --git a/crates/bitvm2-ga/src/watchtower/api.rs b/crates/bitvm2-ga/src/watchtower/api.rs deleted file mode 100644 index 468769f3..00000000 --- a/crates/bitvm2-ga/src/watchtower/api.rs +++ /dev/null @@ -1,74 +0,0 @@ -use anyhow::{Result, bail}; -use bitcoin::{Address, Amount, OutPoint, Transaction, XOnlyPublicKey, key::Keypair}; -use goat::{ - connectors::watchtower_connectors::{AckConnector, WatchtowerChallengeConnector}, - transactions::{ - base::Input, pre_signed::PreSignedTransaction, watchtower_challenge::watchtower_challenge, - }, -}; - -use crate::types::Bitvm2Graph; - -pub fn estimate_watchtower_challenge_vbytes(commitment_data_len: usize) -> usize { - let base_vbytes = 120; - let commitment_vbytes = commitment_data_len * 12 / 10; // assuming 1.2 vbytes per byte of commitment data - base_vbytes + commitment_vbytes -} - -pub fn build_watchtower_challenge_tx( - graph: &Bitvm2Graph, - 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 network = graph.parameters.instance_parameters.network; - let n_of_n_taproot_public_key = - XOnlyPublicKey::from(graph.parameters.instance_parameters.committee_agg_pubkey); - let operator_taproot_public_key = XOnlyPublicKey::from(graph.parameters.operator_pubkey); - let watchtower_taproot_public_key = graph.parameters.watchtower_pubkeys[watchtower_index]; - let hashlock = graph.parameters.hashlocks[watchtower_index]; - let watchtower_connectors = ( - WatchtowerChallengeConnector::new( - network, - &operator_taproot_public_key, - &watchtower_taproot_public_key, - ), - AckConnector::new(network, &n_of_n_taproot_public_key, &hashlock), - ); - if XOnlyPublicKey::from_keypair(watchtower_keypair).0 != watchtower_taproot_public_key { - bail!("Watchtower keypair does not match the watchtower public key"); - } - let watchtower_challenge_connector_vout = watchtower_index * 2; - let watchtower_challenge_connector_amount = graph - .watchtower_challenge_init - .tx() - .output - .get(watchtower_challenge_connector_vout) - .ok_or_else(|| anyhow::anyhow!("watchtower index out of bounds"))? - .value; - let input_0 = Input { - outpoint: OutPoint { - txid: graph.watchtower_challenge_init.tx().compute_txid(), - vout: watchtower_challenge_connector_vout as u32, - }, - amount: watchtower_challenge_connector_amount, - }; - match watchtower_challenge( - watchtower_keypair, - &watchtower_connectors, - commitment_data, - input_0, - payer_inputs, - change_address, - fee_amount, - ) { - Ok(tx) => Ok(tx), - Err(e) => bail!("Failed to build watchtower challenge transaction: {e}"), - } -} diff --git a/crates/client/src/goat_chain/chain_adaptor.rs b/crates/client/src/goat_chain/chain_adaptor.rs index bc3e7d34..f962d5bb 100644 --- a/crates/client/src/goat_chain/chain_adaptor.rs +++ b/crates/client/src/goat_chain/chain_adaptor.rs @@ -102,7 +102,12 @@ pub trait ChainAdaptor: Send + Sync { instance_id: &[u8; 16], graph_id: &[u8; 16], ) -> anyhow::Result; - async fn gateway_cancel_withdraw(&self, graph_id: &[u8; 16]) -> anyhow::Result; + async fn gateway_cancel_withdraw( + &self, + graph_id: &[u8; 16], + nonce: U256, + committee_signs: &[Vec], + ) -> anyhow::Result; async fn gateway_process_withdraw( &self, graph_id: &[u8; 16], @@ -219,6 +224,8 @@ pub trait ChainAdaptor: Send + Sync { async fn committee_mana_is_validate_peer_id(&self, peer_id: &[u8]) -> anyhow::Result; async fn committee_mana_get_watchtowers(&self) -> anyhow::Result>; + async fn committee_mana_get_verifiers(&self) -> anyhow::Result>>; + async fn committee_mana_is_verifier(&self, peer_id: &[u8]) -> anyhow::Result; async fn committee_mana_add_watchtower( &self, watchtower: &[u8; 32], @@ -267,12 +274,12 @@ pub enum GoatNetwork { #[derive(Copy, Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Display, EnumString)] pub enum DisproveTxType { - AssertTimeout, - OperatorCommitTimeout, - OperatorNack, Disprove, QuickChallenge, ChallengeIncompleteKickoff, + PubinDisprove, + OperatorChallengeNack, + OperatorCommitTimeout, } #[derive(Eq, PartialEq, Clone, Debug, Serialize, Deserialize, Display)] @@ -363,9 +370,12 @@ pub struct GraphData { pub kickoff_txid: [u8; 32], pub take1_txid: [u8; 32], pub take2_txid: [u8; 32], - pub commit_timout_txid: [u8; 32], - pub assert_timeout_txids: Vec<[u8; 32]>, - pub nack_txids: Vec<[u8; 32]>, + pub watchtower_challenge_init_txid: [u8; 32], + pub prover_assert_txid: [u8; 32], + pub disprove_txids: Vec<[u8; 32]>, + pub watchtower_challenge_timeout_txids: Vec<[u8; 32]>, + pub operator_challenge_nack_txids: Vec<[u8; 32]>, + pub operator_commit_timeout_txid: [u8; 32], } #[derive(Clone, Debug)] diff --git a/crates/client/src/goat_chain/evmchain.rs b/crates/client/src/goat_chain/evmchain.rs index 5b119c9d..b3c80e82 100644 --- a/crates/client/src/goat_chain/evmchain.rs +++ b/crates/client/src/goat_chain/evmchain.rs @@ -186,8 +186,13 @@ impl EvmChain { self.adaptor.gateway_init_withdraw(instance_id.as_bytes(), graph_id.as_bytes()).await } - pub async fn gateway_cancel_withdraw(&self, graph_id: &Uuid) -> anyhow::Result { - self.adaptor.gateway_cancel_withdraw(graph_id.as_bytes()).await + pub async fn gateway_cancel_withdraw( + &self, + graph_id: &Uuid, + nonce: U256, + committee_signs: &[Vec], + ) -> anyhow::Result { + self.adaptor.gateway_cancel_withdraw(graph_id.as_bytes(), nonce, committee_signs).await } pub async fn gateway_process_withdraw( @@ -423,6 +428,15 @@ impl EvmChain { pub async fn committee_mana_get_watchtowers(&self) -> anyhow::Result> { self.adaptor.committee_mana_get_watchtowers().await } + + pub async fn committee_mana_get_verifiers(&self) -> anyhow::Result>> { + self.adaptor.committee_mana_get_verifiers().await + } + + pub async fn committee_mana_is_verifier(&self, peer_id: &[u8]) -> anyhow::Result { + self.adaptor.committee_mana_is_verifier(peer_id).await + } + pub async fn committee_mana_add_watchtower( &self, watchtower: &[u8; 32], diff --git a/crates/client/src/goat_chain/goat_adaptor.rs b/crates/client/src/goat_chain/goat_adaptor.rs index babad5f0..e25b74b1 100644 --- a/crates/client/src/goat_chain/goat_adaptor.rs +++ b/crates/client/src/goat_chain/goat_adaptor.rs @@ -59,12 +59,12 @@ sol!( #[sol(rpc)] interface IGateway { enum DisproveTxType { - AssertTimeout, - OperatorCommitTimeout, - OperatorNack, Disprove, QuickChallenge, - ChallengeIncompleteKickoff + ChallengeIncompleteKickoff, + PubinDisprove, + OperatorChallengeNack, + OperatorCommitTimeout } enum PeginStatus { None, @@ -73,7 +73,7 @@ sol!( Processing, Locked, Claimed, - Discarded, + Discarded } enum WithdrawStatus { None, @@ -121,9 +121,12 @@ sol!( bytes32 kickoffTxid; bytes32 take1Txid; bytes32 take2Txid; - bytes32 commitTimoutTxid; - bytes32[] assertTimoutTxids; - bytes32[] NackTxids; + bytes32 watchtowerChallengeInitTxid; + bytes32 proverAssertTxid; + bytes32[] disproveTxids; + bytes32[] watchtowerChallengeTimeoutTxids; + bytes32[] operatorChallengeNackTxids; + bytes32 operatorCommitTimeoutTxid; } struct BitcoinTx { @@ -147,10 +150,10 @@ sol!( uint64 public peginFeeRate; uint64 public minOperatorRewardSats; uint64 public operatorRewardRate; - uint64 public minStakeAmount; - uint64 public minChallengerReward; - uint64 public minDisproverReward; - uint64 public minSlashAmount; + uint256 public minStakeAmount; + uint256 public minChallengerReward; + uint256 public minDisproverReward; + uint256 public minSlashAmount; address public pegBTC; address public bitcoinSPV; @@ -171,19 +174,21 @@ sol!( function postGraphData(bytes16 instanceId, bytes16 graphId, GraphData calldata graphData, bytes[] calldata committeeSigs) public; function getGraphData(bytes16 graphId) external view returns (GraphData memory); function initWithdraw(bytes16 instanceId, bytes16 graphId) external; - function cancelWithdraw(bytes16 graphId) external; + function committeeCancelWithdraw(bytes16 graphId, uint256 nonce, bytes[] calldata committeeSigs) external; function proceedWithdraw(bytes16 graphId, BitcoinTx calldata rawKickoffTx, BitcoinTxProof calldata kickoffProof) external; function finishWithdrawHappyPath(bytes16 graphId, BitcoinTx calldata rawTake1Tx, BitcoinTxProof calldata take1Proof) external; function finishWithdrawUnhappyPath(bytes16 graphId, BitcoinTx calldata rawTake2Tx, BitcoinTxProof calldata take2Proof) external; function finishWithdrawDisproved(bytes16 graphId, DisproveTxType disproveTxType, uint256 txnIndex, BitcoinTx calldata rawChallengeStartTx, BitcoinTxProof calldata challengeStartTxProof, BitcoinTx calldata rawChallengeFinishTx, BitcoinTxProof calldata challengeFinishTxProof ) external; function getCommitteePubkeys(bytes16 instanceId) public view returns (bytes[] memory committeePubkeys); + function getCommitteeAddresses(bytes16 instanceId) public view returns (address[] memory committeeAddresses); + function getCommitteePubkeysUnsafe(bytes16 instanceId) public view returns (bytes[] memory committeePubkeys); function getPostGraphDigest(bytes16 instanceId, bytes16 graphId, GraphData calldata graphData) public view returns (bytes32); function getPostPeginDigest(bytes16 instanceId, bytes32 peginTxid) public view returns (bytes32); function getGraphIdsByInstanceId(bytes16 instanceId) external view returns (bytes16[] memory); + function getCancelWithdrawDigestNonced(bytes16 graphId, uint256 nonce) public view returns (bytes32); + function getUnlockStakeDigestNonced(address operator, uint256 amount, uint256 nonce) public view returns (bytes32); + function unlockOperatorStake(address operator, uint256 amount, uint256 nonce, bytes[] calldata committeeSigs) external; - // Contract is not implements this functions, do something later - function getInitializedInstanceIds() external view returns (bytes16[] memory retInstanceIds, bytes16[] memory retGraphIds); - function getInstanceIdsByPubKey(bytes32 operatorPubkey) external view returns (bytes16[] memory retInstanceIds, bytes16[] memory retGraphIds); } ); @@ -263,6 +268,8 @@ sol!( function balanceOf(address account) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); + function mint(address to, uint256 amount) external; + function burn(uint256 amount) external; } ); @@ -303,6 +310,9 @@ sol!( function slashStake(address operator, uint256 amount) external; function lockStake(address operator, uint256 amount) external; function unlockStake(address operator, uint256 amount) external; + function stake(uint256 amount) external; + function unstake(uint256 amount) external; + function registerPubkey(bytes32 pubkey) external; } ); @@ -315,11 +325,23 @@ sol!( function committeeSize() external view returns (uint256); function quorumSize() external view returns (uint256); function verifySignatures(bytes32 msgHash, bytes[] memory signatures) external view returns (bool); + function registerPeerId(bytes calldata peerId) external; function getCommitteePeerId(address member) external view returns (bytes); - function isValidPeerId(bytes peerId) external view returns (bool); + function isValidPeerId(bytes calldata peerId) external view returns (bool); function getWatchtowers() external view returns (bytes32[] memory); + function getVerifiers() external view returns (bytes[] memory); + function isVerifier(bytes calldata peerId) external view returns (bool); function addWatchtower(bytes32 watchtower, uint256 nonce, bytes[] memory authSignatures) external; function removeWatchtower(bytes32 watchtower, uint256 nonce, bytes[] memory authSignatures) external; + function getNoncedDigest(bytes32 msgHash, uint256 nonce) external view returns (bytes32); + function getAddWatchtowerDigestNonced(bytes32 watchtower, uint256 nonce) external view returns (bytes32); + function getRemoveWatchtowerDigestNonced(bytes32 watchtower, uint256 nonce) external view returns (bytes32); + function isAuthorizedCaller(address caller) external view returns (bool); + function addAuthorizedCaller(address caller, uint256 nonce, bytes[] memory authSignatures) external; + function removeAuthorizedCaller(address caller, uint256 nonce, bytes[] memory authSignatures) external; + function getAddAuthorizedCallerDigestNonced(address caller, uint256 nonce) external view returns (bytes32); + function getRemoveAuthorizedCallerDigestNonced(address caller, uint256 nonce) external view returns (bytes32); + function executeNoncedSignatures(bytes32 msgHash, uint256 nonce, bytes[] memory signatures) external; } ); @@ -489,7 +511,7 @@ impl GoatAdaptor { fn get_committee_management(&self) -> anyhow::Result<&CommitteeManagementInstance> { self.committee_management .as_ref() - .ok_or_else(|| anyhow::anyhow!("CommitteeMnagement not initialized")) + .ok_or_else(|| anyhow::anyhow!("CommitteeManagement not initialized")) } fn get_stake_management(&self) -> anyhow::Result<&StakeManagementInstance> { @@ -744,16 +766,18 @@ impl From for WithdrawStatus { impl From for IGateway::DisproveTxType { fn from(value: DisproveTxType) -> Self { match value { - DisproveTxType::OperatorNack => IGateway::DisproveTxType::OperatorNack, - DisproveTxType::OperatorCommitTimeout => { - IGateway::DisproveTxType::OperatorCommitTimeout - } - DisproveTxType::AssertTimeout => IGateway::DisproveTxType::AssertTimeout, DisproveTxType::Disprove => IGateway::DisproveTxType::Disprove, DisproveTxType::QuickChallenge => IGateway::DisproveTxType::QuickChallenge, DisproveTxType::ChallengeIncompleteKickoff => { IGateway::DisproveTxType::ChallengeIncompleteKickoff } + DisproveTxType::PubinDisprove => IGateway::DisproveTxType::PubinDisprove, + DisproveTxType::OperatorChallengeNack => { + IGateway::DisproveTxType::OperatorChallengeNack + } + DisproveTxType::OperatorCommitTimeout => { + IGateway::DisproveTxType::OperatorCommitTimeout + } } } } @@ -807,17 +831,26 @@ impl From for IGateway::GraphData { kickoffTxid: FixedBytes::from_slice(&value.kickoff_txid), take1Txid: FixedBytes::from_slice(&value.take1_txid), take2Txid: FixedBytes::from_slice(&value.take2_txid), - commitTimoutTxid: FixedBytes::from_slice(&value.commit_timout_txid), - assertTimoutTxids: value - .assert_timeout_txids + watchtowerChallengeInitTxid: FixedBytes::from_slice( + &value.watchtower_challenge_init_txid, + ), + proverAssertTxid: FixedBytes::from_slice(&value.prover_assert_txid), + disproveTxids: value + .disprove_txids + .into_iter() + .map(|txid| FixedBytes::from_slice(&txid)) + .collect::>(), + watchtowerChallengeTimeoutTxids: value + .watchtower_challenge_timeout_txids .into_iter() .map(|txid| FixedBytes::from_slice(&txid)) .collect::>(), - NackTxids: value - .nack_txids + operatorChallengeNackTxids: value + .operator_challenge_nack_txids .into_iter() .map(|txid| FixedBytes::from_slice(&txid)) .collect::>(), + operatorCommitTimeoutTxid: FixedBytes::from_slice(&value.operator_commit_timeout_txid), } } } @@ -831,13 +864,20 @@ impl From for GraphData { kickoff_txid: value.kickoffTxid.0, take1_txid: value.take1Txid.0, take2_txid: value.take2Txid.0, - commit_timout_txid: value.commitTimoutTxid.0, - assert_timeout_txids: value - .assertTimoutTxids + watchtower_challenge_init_txid: value.watchtowerChallengeInitTxid.0, + prover_assert_txid: value.proverAssertTxid.0, + disprove_txids: value.disproveTxids.into_iter().map(|txid| txid.into()).collect(), + watchtower_challenge_timeout_txids: value + .watchtowerChallengeTimeoutTxids + .into_iter() + .map(|txid| txid.into()) + .collect(), + operator_challenge_nack_txids: value + .operatorChallengeNackTxids .into_iter() .map(|txid| txid.into()) .collect(), - nack_txids: value.NackTxids.into_iter().map(|txid| txid.into()).collect(), + operator_commit_timeout_txid: value.operatorCommitTimeoutTxid.0, } } } @@ -981,22 +1021,22 @@ impl ChainAdaptor for GoatAdaptor { async fn gateway_get_min_stake_amount(&self) -> anyhow::Result { let gateway = self.get_gateway()?; - Ok(gateway.minStakeAmount().call().await?) + Ok(gateway.minStakeAmount().call().await?.try_into()?) } async fn gateway_get_min_challenger_reward(&self) -> anyhow::Result { let gateway = self.get_gateway()?; - Ok(gateway.minChallengerReward().call().await?) + Ok(gateway.minChallengerReward().call().await?.try_into()?) } async fn gateway_get_min_disprover_reward(&self) -> anyhow::Result { let gateway = self.get_gateway()?; - Ok(gateway.minDisproverReward().call().await?) + Ok(gateway.minDisproverReward().call().await?.try_into()?) } async fn gateway_get_min_slash_amount(&self) -> anyhow::Result { let gateway = self.get_gateway()?; - Ok(gateway.minSlashAmount().call().await?) + Ok(gateway.minSlashAmount().call().await?.try_into()?) } async fn gateway_get_committee_management(&self) -> anyhow::Result<[u8; 20]> { @@ -1136,29 +1176,14 @@ impl ChainAdaptor for GoatAdaptor { } async fn gateway_get_initialized_ids(&self) -> anyhow::Result> { - let gateway = self.get_gateway()?; - let ids = gateway.getInitializedInstanceIds().call().await?; - let instance_ids: Vec = - ids.retInstanceIds.iter().map(|v| Uuid::from_bytes(v.0)).collect(); - let graph_ids: Vec = - ids.retGraphIds.into_iter().map(|v| Uuid::from_bytes(v.0)).collect(); - Ok(instance_ids.into_iter().zip(graph_ids).collect()) + bail!("Gateway.getInitializedInstanceIds is not available in the gc contract interface") } async fn gateway_get_instanceids_by_pubkey( &self, - operator_pubkey: &[u8; 32], + _operator_pubkey: &[u8; 32], ) -> anyhow::Result> { - let gateway = self.get_gateway()?; - let ids = gateway - .getInstanceIdsByPubKey(FixedBytes::<32>::from_slice(operator_pubkey)) - .call() - .await?; - let instance_ids: Vec = - ids.retInstanceIds.iter().map(|v| Uuid::from_bytes(v.0)).collect(); - let graph_ids: Vec = - ids.retGraphIds.into_iter().map(|v| Uuid::from_bytes(v.0)).collect(); - Ok(instance_ids.into_iter().zip(graph_ids).collect()) + bail!("Gateway.getInstanceIdsByPubKey is not available in the gc contract interface") } async fn gateway_init_withdraw( @@ -1176,10 +1201,16 @@ impl ChainAdaptor for GoatAdaptor { Ok(tx_hash.to_string()) } - async fn gateway_cancel_withdraw(&self, graph_id: &[u8; 16]) -> anyhow::Result { + async fn gateway_cancel_withdraw( + &self, + graph_id: &[u8; 16], + nonce: U256, + committee_signs: &[Vec], + ) -> anyhow::Result { let gateway = self.get_gateway()?; + let signs: Vec = committee_signs.iter().map(|v| Bytes::copy_from_slice(v)).collect(); let tx_request = gateway - .cancelWithdraw(FixedBytes::from_slice(graph_id)) + .committeeCancelWithdraw(FixedBytes::from_slice(graph_id), nonce, signs) .from(self.get_default_signer_address()) .chain_id(self.chain_id) .into_transaction_request(); @@ -1531,6 +1562,22 @@ impl ChainAdaptor for GoatAdaptor { .collect::>()) } + async fn committee_mana_get_verifiers(&self) -> anyhow::Result>> { + let committee_management = self.get_committee_management()?; + Ok(committee_management + .getVerifiers() + .call() + .await? + .into_iter() + .map(|v| v.to_vec()) + .collect::>>()) + } + + async fn committee_mana_is_verifier(&self, peer_id: &[u8]) -> anyhow::Result { + let committee_management = self.get_committee_management()?; + Ok(committee_management.isVerifier(Bytes::copy_from_slice(peer_id)).call().await?) + } + async fn committee_mana_add_watchtower( &self, watchtower: &[u8; 32], diff --git a/crates/client/src/goat_chain/mock_goat_adaptor.rs b/crates/client/src/goat_chain/mock_goat_adaptor.rs index 7683147d..e00e1a13 100644 --- a/crates/client/src/goat_chain/mock_goat_adaptor.rs +++ b/crates/client/src/goat_chain/mock_goat_adaptor.rs @@ -266,7 +266,12 @@ impl ChainAdaptor for MockAdaptor { Ok(hex::encode(generate_random_bytes(32))) } - async fn gateway_cancel_withdraw(&self, _graph_id: &[u8; 16]) -> anyhow::Result { + async fn gateway_cancel_withdraw( + &self, + _graph_id: &[u8; 16], + _nonce: U256, + _committee_signs: &[Vec], + ) -> anyhow::Result { info!("call cancel_withdraw"); Ok(hex::encode(generate_random_bytes(32))) } @@ -461,6 +466,14 @@ impl ChainAdaptor for MockAdaptor { Ok(vec![]) } + async fn committee_mana_get_verifiers(&self) -> anyhow::Result>> { + Ok(vec![]) + } + + async fn committee_mana_is_verifier(&self, _peer_id: &[u8]) -> anyhow::Result { + Ok(false) + } + async fn committee_mana_add_watchtower( &self, _watchtower: &[u8; 32], diff --git a/crates/client/src/goat_chain/mod.rs b/crates/client/src/goat_chain/mod.rs index 67ab134f..e975f568 100644 --- a/crates/client/src/goat_chain/mod.rs +++ b/crates/client/src/goat_chain/mod.rs @@ -252,8 +252,13 @@ impl GOATClient { self.chain_service.gateway_init_withdraw(instance_id, graph_id).await } - pub async fn gateway_cancel_withdraw(&self, graph_id: &Uuid) -> anyhow::Result { - self.chain_service.gateway_cancel_withdraw(graph_id).await + pub async fn gateway_cancel_withdraw( + &self, + graph_id: &Uuid, + nonce: U256, + committee_signs: &[Vec], + ) -> anyhow::Result { + self.chain_service.gateway_cancel_withdraw(graph_id, nonce, committee_signs).await } pub async fn gateway_process_withdraw( @@ -365,10 +370,14 @@ impl GOATClient { (raw_challenge_start_tx, challenge_start_proof) } None => { - // if no challengeStartTx happens (for QuickChallenge & ChallengeIncompleteKickoff), set rawChallengeStartTx.inputVector to empty + // if no challengeStartTx happens, set rawChallengeStartTx.inputVector to empty if !matches!( disprove_type, - DisproveTxType::QuickChallenge | DisproveTxType::ChallengeIncompleteKickoff + DisproveTxType::QuickChallenge + | DisproveTxType::ChallengeIncompleteKickoff + | DisproveTxType::PubinDisprove + | DisproveTxType::OperatorChallengeNack + | DisproveTxType::OperatorCommitTimeout ) { bail!("challenge_start_tx is required for disprove type {disprove_type:?}"); } @@ -741,6 +750,15 @@ impl GOATClient { .filter_map(|v| XOnlyPublicKey::from_slice(v).ok()) .collect::>()) } + + pub async fn committee_mana_get_verifiers(&self) -> anyhow::Result>> { + self.chain_service.committee_mana_get_verifiers().await + } + + pub async fn committee_mana_is_verifier(&self, peer_id: &[u8]) -> anyhow::Result { + self.chain_service.committee_mana_is_verifier(peer_id).await + } + pub async fn committee_mana_add_watchtower( &self, watchtower: &[u8; 32], diff --git a/crates/client/src/goat_chain/utils.rs b/crates/client/src/goat_chain/utils.rs index e7fcadcb..c000a5fa 100644 --- a/crates/client/src/goat_chain/utils.rs +++ b/crates/client/src/goat_chain/utils.rs @@ -23,7 +23,7 @@ sol!( #[allow(missing_docs)] #[sol(rpc)] interface ICommitteeManagement { - function isValidPeerId(bytes peerId) external view returns (bool); + function isValidPeerId(bytes calldata peerId) external view returns (bool); } ); diff --git a/crates/commit-chain/Cargo.toml b/crates/commit-chain/Cargo.toml index 05a2c247..b981bd7c 100644 --- a/crates/commit-chain/Cargo.toml +++ b/crates/commit-chain/Cargo.toml @@ -9,11 +9,11 @@ bitcoin = { workspace = true, features = ["serde", "rand", "secp-recovery"] } bincode = "1.3.3" # workspace -guest-executor = { workspace = true } +guest-executor = { workspace = true } #header-chain = { path = "../header-chain" } # Ziren -zkm-verifier = { workspace = true } +verifier = { workspace = true } zkm-zkvm = { workspace = true } #zkm-verifier = { path = "../../../Ziren/crates/verifier" } #zkm-zkvm = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } @@ -22,7 +22,7 @@ zkm-zkvm = { 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, features = ["serde", "bn"], default-features = false } sha2 = "0.10.9" @@ -37,14 +37,14 @@ serde = { workspace = true, features = ["derive"] } #cosmos-sdk-proto = { version = "0.27", default-features = false, features = ["serde"] } 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", ] } [dev-dependencies] rand = "0.8.5" serde_json = "1.0" -borsh = {version = "1.5.3", features = ["derive"] } +borsh = { version = "1.5.3", features = ["derive"] } blake3 = "1.6.1" [build-dependencies] diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index 18e3519a..bb51246e 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -9,7 +9,7 @@ pub use tendermint_light_client_verifier::{ types::{Hash, ValidatorSet}, }; -use bitcoin::{Transaction, TxOut, Witness, secp256k1::PublicKey}; +use bitcoin::{Transaction, TxOut, Witness, hashes::Hash as _, secp256k1::PublicKey}; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct CommitInfo { @@ -99,6 +99,7 @@ pub struct CommitChainState { pub sequencers: Vec, pub publisher_public_keys: Vec, pub threshold: u16, + pub operator_vk_hash: [u8; 32], } impl CircuitCommit { @@ -114,12 +115,54 @@ impl CircuitCommit { pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; pub const VK_HASH_SIZE: usize = 66; +pub const LEGACY_COMMIT_CHAIN_COMMITMENT_SIZE: usize = 64; +pub const COMMIT_CHAIN_COMMITMENT_SIZE: usize = 96; +pub const LEGACY_OPERATOR_VK_HASH: [u8; 32] = [0u8; 32]; + +#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] +pub struct CommitChainCommitment { + pub sequencer_set_hash: [u8; 32], + pub genesis_evm_block_hash: [u8; 32], + pub operator_vk_hash: [u8; 32], +} #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct CommitChainCircuitOutput { pub chain_state: CommitChainState, } +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +struct LegacyCommitChainState { + block_height: u32, + commit_txn: Transaction, + genesis_txid: [u8; 32], + sequencers: Vec, + publisher_public_keys: Vec, + threshold: u16, +} + +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +struct LegacyCommitChainCircuitOutput { + chain_state: LegacyCommitChainState, +} + +impl From for CommitChainCircuitOutput { + fn from(output: LegacyCommitChainCircuitOutput) -> Self { + let chain_state = output.chain_state; + CommitChainCircuitOutput { + chain_state: CommitChainState { + block_height: chain_state.block_height, + commit_txn: chain_state.commit_txn, + genesis_txid: chain_state.genesis_txid, + sequencers: chain_state.sequencers, + publisher_public_keys: chain_state.publisher_public_keys, + threshold: chain_state.threshold, + operator_vk_hash: LEGACY_OPERATOR_VK_HASH, + }, + } + } +} + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct CommitChainCircuitInput { pub prev_proof: CommitChainPrevProofType, @@ -136,6 +179,40 @@ pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> Hash { sequencer_set.hash() } +pub fn parse_commit_chain_commitment(commitment: &[u8]) -> CommitChainCommitment { + assert!( + commitment.len() == LEGACY_COMMIT_CHAIN_COMMITMENT_SIZE + || commitment.len() == COMMIT_CHAIN_COMMITMENT_SIZE, + "commit chain commitment must be 64 or 96 bytes" + ); + + let mut sequencer_set_hash = [0u8; 32]; + sequencer_set_hash.copy_from_slice(&commitment[0..32]); + let mut genesis_evm_block_hash = [0u8; 32]; + genesis_evm_block_hash.copy_from_slice(&commitment[32..64]); + let mut operator_vk_hash = LEGACY_OPERATOR_VK_HASH; + if commitment.len() == COMMIT_CHAIN_COMMITMENT_SIZE { + operator_vk_hash.copy_from_slice(&commitment[64..]); + assert_ne!( + operator_vk_hash, LEGACY_OPERATOR_VK_HASH, + "new commit chain commitment must include non-zero operator vk hash" + ); + } + + CommitChainCommitment { sequencer_set_hash, genesis_evm_block_hash, operator_vk_hash } +} + +/// Decode current or legacy commit-chain public values. +pub fn decode_commit_chain_circuit_output(public_values: &[u8]) -> CommitChainCircuitOutput { + if let Ok(output) = bincode::deserialize::(public_values) { + return output; + } + + bincode::deserialize::(public_values) + .map(Into::into) + .expect("failed to decode commit chain circuit output as current or legacy format") +} + impl CommitChainState { pub fn new(genesis_txid: [u8; 32]) -> Self { CommitChainState { @@ -150,6 +227,7 @@ impl CommitChainState { sequencers: Vec::new(), publisher_public_keys: vec![], threshold: u16::MAX, + operator_vk_hash: [0u8; 32], } } @@ -158,25 +236,34 @@ impl CommitChainState { let mut latest_commit_txn_with_wtns = commit.commit_txn.clone(); let latest_sequencers = &commit.sequencers; let (next_publisher_public_keys, next_threshold) = commit.active_publisher_set(); + let has_prev_commit = !self.commit_txn.output.is_empty(); assert_eq!(commit.genesis_txid, self.genesis_txid); + if !has_prev_commit { + assert_eq!( + latest_commit_txn_with_wtns.compute_txid().as_raw_hash().to_byte_array(), + self.genesis_txid + ); + } // calculate the commitment of latest sequencer set and check the equivalent let expected_latest_commit = extract_op_return_data(&latest_commit_txn_with_wtns.output); + let latest_commitment = parse_commit_chain_commitment(&expected_latest_commit); if let Hash::Sha256(latest_sequencer_set_hash) = sequencer_hash(latest_sequencers) { - assert_eq!(latest_sequencer_set_hash[..], expected_latest_commit[0..32]); + assert_eq!(latest_sequencer_set_hash, latest_commitment.sequencer_set_hash); } else { panic!("Invalid latest sequencer set hash"); } // check the latest txn's prev out is equals to the output of prev_txn let prev_commit_txn_value = &self.commit_txn; - if !self.sequencers.is_empty() { + if has_prev_commit { // calculate the commitment of prev sequencer set and check the equivalent let expected_prev_commit = extract_op_return_data(&prev_commit_txn_value.output); + let prev_commitment = parse_commit_chain_commitment(&expected_prev_commit); if let Hash::Sha256(prev_sequencer_set_hash) = sequencer_hash(&self.sequencers) { - assert_eq!(prev_sequencer_set_hash[..], expected_prev_commit[0..32]); + assert_eq!(prev_sequencer_set_hash, prev_commitment.sequencer_set_hash); } else { panic!("Invalid prev sequencer set hash"); } @@ -224,6 +311,7 @@ impl CommitChainState { self.publisher_public_keys = next_publisher_public_keys.to_vec(); self.threshold = next_threshold; self.block_height = commit.block_height; + self.operator_vk_hash = latest_commitment.operator_vk_hash; } } } @@ -273,7 +361,7 @@ mod tests { use crate::{ create_dummy_publisher_keys, create_sequencer_update_script, finalize, sign_partial, }; - use bitcoin::{Amount, ScriptBuf}; + use bitcoin::{Amount, ScriptBuf, script::PushBytesBuf}; use bitcoin::{ EcdsaSighashType, OutPoint, Sequence, TxIn, Witness, absolute::LockTime, transaction::Version, @@ -294,30 +382,132 @@ mod tests { assert_eq!(expected_op_data.to_vec(), op_return_data); } + fn commitment_payload( + sequencers: &[SequencerInfo], + genesis_evm_block_hash: [u8; 32], + operator_vk_hash: [u8; 32], + ) -> PushBytesBuf { + let mut payload = Vec::with_capacity(96); + if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = + sequencer_hash(sequencers) + { + payload.extend_from_slice(&hash); + } else { + panic!("expected sha256 sequencer hash"); + }; + payload.extend_from_slice(&genesis_evm_block_hash); + payload.extend_from_slice(&operator_vk_hash); + PushBytesBuf::try_from(payload).expect("commitment payload is pushable") + } + #[test] - fn test_apply_commit() { - let commit_info: Vec = serde_json::from_slice(include_bytes!( - "../../../circuits/data/commit-chain/0-1.bin.commits" - )) - .unwrap(); + fn test_parse_commit_chain_commitment_splits_96_byte_payload() { + let sequencer_set_hash = [0x11u8; 32]; + let genesis_evm_block_hash = [0x22u8; 32]; + let operator_vk_hash = [0x33u8; 32]; + let mut payload = Vec::with_capacity(96); + payload.extend_from_slice(&sequencer_set_hash); + payload.extend_from_slice(&genesis_evm_block_hash); + payload.extend_from_slice(&operator_vk_hash); + + let commitment = parse_commit_chain_commitment(&payload); + + assert_eq!(commitment.sequencer_set_hash, sequencer_set_hash); + assert_eq!(commitment.genesis_evm_block_hash, genesis_evm_block_hash); + assert_eq!(commitment.operator_vk_hash, operator_vk_hash); + } - let mut chain_state = CommitChainState::new(commit_info[0].genesis_txid); - chain_state.apply_commit(commit_info.clone()); - assert_eq!(commit_info[0].genesis_txid, chain_state.genesis_txid); - assert_eq!(commit_info[0].sequencers.clone(), chain_state.sequencers.clone()); - assert_eq!(commit_info[0].commit_txn.compute_txid(), chain_state.commit_txn.compute_txid()); + #[test] + fn test_parse_commit_chain_commitment_accepts_legacy_64_byte_payload() { + let sequencer_set_hash = [0x11u8; 32]; + let genesis_evm_block_hash = [0x22u8; 32]; + let mut payload = Vec::with_capacity(64); + payload.extend_from_slice(&sequencer_set_hash); + payload.extend_from_slice(&genesis_evm_block_hash); + + let commitment = parse_commit_chain_commitment(&payload); + + assert_eq!(commitment.sequencer_set_hash, sequencer_set_hash); + assert_eq!(commitment.genesis_evm_block_hash, genesis_evm_block_hash); + assert_eq!(commitment.operator_vk_hash, LEGACY_OPERATOR_VK_HASH); + } - let commit_info2: Vec = serde_json::from_slice(include_bytes!( - "../../../circuits/data/commit-chain/1-1.bin.commits" - )) - .unwrap(); - chain_state.apply_commit(commit_info2.clone()); - assert_eq!(commit_info[0].genesis_txid, chain_state.genesis_txid); - assert_eq!(commit_info2[0].sequencers.clone(), chain_state.sequencers.clone()); - assert_eq!( - commit_info2[0].commit_txn.compute_txid(), - chain_state.commit_txn.compute_txid() - ); + #[test] + fn test_parse_commit_chain_commitment_rejects_new_payload_with_zero_operator_vk_hash() { + let mut payload = vec![0x11u8; 96]; + payload[64..].fill(0); + + let result = std::panic::catch_unwind(|| parse_commit_chain_commitment(&payload)); + + assert!(result.is_err()); + } + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct LegacyCommitChainState { + block_height: u32, + commit_txn: Transaction, + genesis_txid: [u8; 32], + sequencers: Vec, + publisher_public_keys: Vec, + threshold: u16, + } + + #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] + struct LegacyCommitChainCircuitOutput { + chain_state: LegacyCommitChainState, + } + + #[test] + fn test_decode_commit_chain_circuit_output_accepts_legacy_public_values() { + let legacy_output = LegacyCommitChainCircuitOutput { + chain_state: LegacyCommitChainState { + block_height: 7, + commit_txn: Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + }, + genesis_txid: [0x22u8; 32], + sequencers: vec![], + publisher_public_keys: vec![], + threshold: 0, + }, + }; + let public_values = bincode::serialize(&legacy_output).unwrap(); + + let decoded = decode_commit_chain_circuit_output(&public_values); + + assert_eq!(decoded.chain_state.block_height, legacy_output.chain_state.block_height); + assert_eq!(decoded.chain_state.genesis_txid, legacy_output.chain_state.genesis_txid); + assert_eq!(decoded.chain_state.operator_vk_hash, LEGACY_OPERATOR_VK_HASH); + } + + // todo: use new commit file + #[test] + fn test_apply_commit() { + // let commit_info: Vec = serde_json::from_slice(include_bytes!( + // "../../../circuits/data/commit-chain/0-1.bin.commits" + // )) + // .unwrap(); + // + // let mut chain_state = CommitChainState::new(commit_info[0].genesis_txid); + // chain_state.apply_commit(commit_info.clone()); + // assert_eq!(commit_info[0].genesis_txid, chain_state.genesis_txid); + // assert_eq!(commit_info[0].sequencers.clone(), chain_state.sequencers.clone()); + // assert_eq!(commit_info[0].commit_txn.compute_txid(), chain_state.commit_txn.compute_txid()); + // + // let commit_info2: Vec = serde_json::from_slice(include_bytes!( + // "../../../circuits/data/commit-chain/1-1.bin.commits" + // )) + // .unwrap(); + // chain_state.apply_commit(commit_info2.clone()); + // assert_eq!(commit_info[0].genesis_txid, chain_state.genesis_txid); + // assert_eq!(commit_info2[0].sequencers.clone(), chain_state.sequencers.clone()); + // assert_eq!( + // commit_info2[0].commit_txn.compute_txid(), + // chain_state.commit_txn.compute_txid() + // ); } #[test] @@ -333,13 +523,13 @@ mod tests { let next_threshold = 3u16; let final_threshold = 4u16; let empty_sequencers = vec![]; - let commit0_op_return = if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = - sequencer_hash(&empty_sequencers) - { - ScriptBuf::new_op_return(&hash) - } else { - panic!("expected sha256 sequencer hash"); - }; + let genesis_evm_block_hash = [0x11u8; 32]; + let operator_vk_hash = [0x22u8; 32]; + let commit0_op_return = ScriptBuf::new_op_return(commitment_payload( + &empty_sequencers, + genesis_evm_block_hash, + operator_vk_hash, + )); let commit0 = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, @@ -374,13 +564,12 @@ mod tests { let commit1_redeem_script = create_sequencer_update_script(¤t_pubkeys, current_threshold as usize); - let commit1_op_return = if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = - sequencer_hash(&empty_sequencers) - { - ScriptBuf::new_op_return(&hash) - } else { - panic!("expected sha256 sequencer hash"); - }; + let commit1_operator_vk_hash = [0x33u8; 32]; + let commit1_op_return = ScriptBuf::new_op_return(commitment_payload( + &empty_sequencers, + genesis_evm_block_hash, + commit1_operator_vk_hash, + )); let mut commit1 = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, @@ -431,13 +620,12 @@ mod tests { let commit2_redeem_script = create_sequencer_update_script(&next_pubkeys, next_threshold as usize); - let commit2_op_return = if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = - sequencer_hash(&empty_sequencers) - { - ScriptBuf::new_op_return(&hash) - } else { - panic!("expected sha256 sequencer hash"); - }; + let commit2_operator_vk_hash = [0x44u8; 32]; + let commit2_op_return = ScriptBuf::new_op_return(commitment_payload( + &empty_sequencers, + genesis_evm_block_hash, + commit2_operator_vk_hash, + )); let mut commit2 = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, @@ -498,9 +686,57 @@ mod tests { chain_state.apply_commit(vec![commit0_info]); assert_eq!(chain_state.publisher_public_keys, current_pubkeys); assert_eq!(chain_state.threshold, current_threshold); + assert_eq!(chain_state.operator_vk_hash, operator_vk_hash); chain_state.apply_commit(vec![commit1_info, commit2_info]); assert_eq!(chain_state.publisher_public_keys, final_pubkeys); assert_eq!(chain_state.threshold, final_threshold); + assert_eq!(chain_state.operator_vk_hash, commit2_operator_vk_hash); + } + + #[test] + fn test_apply_commit_tracks_genesis_operator_vk_hash() { + let next_keys = create_dummy_publisher_keys(3, bitcoin::Network::Regtest); + let next_pubkeys: Vec = next_keys.iter().map(|(_, pk)| *pk).collect(); + let empty_sequencers = vec![]; + let genesis_evm_block_hash = [0x55u8; 32]; + let operator_vk_hash = [0x66u8; 32]; + let commit_txn = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![ + TxOut { + value: Amount::from_sat(100_000), + script_pubkey: ScriptBuf::new_p2wsh( + &create_sequencer_update_script(&next_pubkeys, 2).wscript_hash(), + ), + }, + TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::new_op_return(commitment_payload( + &empty_sequencers, + genesis_evm_block_hash, + operator_vk_hash, + )), + }, + ], + }; + let genesis_txid = *commit_txn.compute_txid().as_byte_array(); + let commit = CircuitCommit { + commit_txn, + genesis_txid, + publisher_public_keys: vec![], + threshold: 0, + next_publisher_public_keys: Some(next_pubkeys), + next_threshold: Some(2), + sequencers: empty_sequencers, + block_height: 1, + }; + + let mut chain_state = CommitChainState::new(genesis_txid); + chain_state.apply_commit(vec![commit]); + + assert_eq!(chain_state.operator_vk_hash, operator_vk_hash); } } diff --git a/crates/commit-chain/src/lib.rs b/crates/commit-chain/src/lib.rs index bc9d5d37..bee13b09 100644 --- a/crates/commit-chain/src/lib.rs +++ b/crates/commit-chain/src/lib.rs @@ -2,34 +2,23 @@ mod publisher; pub use publisher::*; mod commit_chain; pub use commit_chain::*; -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; - -pub const TRUSTED_COMMIT_CHAIN_ZKM_VERSION: &str = "v1.2.5"; - -/// Return the fixed `part_stark_vk` used by commit-chain's internal recursive verifier. -pub fn trusted_commit_chain_part_stark_vk() -> Vec { - Groth16Verifier::get_part_stark_vk(TRUSTED_COMMIT_CHAIN_ZKM_VERSION).to_vec() -} pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircuitOutput { - let current_part_stark_vk = trusted_commit_chain_part_stark_vk(); let mut chain_state = match input.prev_proof { CommitChainPrevProofType::GenesisBlock => { CommitChainState::new(input.commits[0].genesis_txid) } CommitChainPrevProofType::PrevProof(prev_proof) => { println!("verify commit chain of prev proof"); - let groth16_vk = *IMM_GROTH16_VK_BYTES; - let zkm_vk_hash = String::from_utf8(input.zkm_vk_hash.to_vec()).unwrap(); - assert_eq!(input.zkm_version, TRUSTED_COMMIT_CHAIN_ZKM_VERSION); - Groth16Verifier::verify_by_imm_groth16_vk( + verifier::verify_groth16_proof( &input.zkm_proof, &input.zkm_public_values, - &zkm_vk_hash, - groth16_vk, - ¤t_part_stark_vk, + &input.zkm_vk_hash, + &input.zkm_version, ) .unwrap(); + + // todo: read from input.zkm_public_values prev_proof.chain_state } }; diff --git a/crates/header-chain/Cargo.toml b/crates/header-chain/Cargo.toml index fed3d23d..d395bb17 100644 --- a/crates/header-chain/Cargo.toml +++ b/crates/header-chain/Cargo.toml @@ -11,7 +11,7 @@ bitcoin = { workspace = true, features = ["serde"] } serde = { workspace = true, default-features = false } crypto-bigint = { version = "0.5.5", default-features = false } zkm-zkvm = { workspace = true } -zkm-verifier = { workspace = true } +verifier = { workspace = true } [dev-dependencies] hex-literal = "1.0.0" diff --git a/crates/header-chain/src/header_chain.rs b/crates/header-chain/src/header_chain.rs index cd7f452a..9bfd2a68 100644 --- a/crates/header-chain/src/header_chain.rs +++ b/crates/header-chain/src/header_chain.rs @@ -363,7 +363,6 @@ fn calculate_work(target: &[u8; 32]) -> U256 { )] pub struct BlockHeaderCircuitOutput { pub chain_state: ChainState, - pub part_stark_vk: Vec, } /// The input proof of the header chain circuit. diff --git a/crates/header-chain/src/lib.rs b/crates/header-chain/src/lib.rs index f2ca0147..0617922f 100644 --- a/crates/header-chain/src/lib.rs +++ b/crates/header-chain/src/lib.rs @@ -12,25 +12,20 @@ pub use transaction::*; pub mod spv; pub use spv::SPV; -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; /// The main entry point of the header chain circuit. pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircuitOutput { - let current_part_stark_vk = Groth16Verifier::get_part_stark_vk(&input.zkm_version).to_vec(); // println!("Detected network: {:?}", NETWORK_TYPE); // println!("NETWORK_CONSTANTS: {:?}", NETWORK_CONSTANTS); let mut chain_state = match input.prev_proof { HeaderChainPrevProofType::GenesisBlock => ChainState::new(), HeaderChainPrevProofType::PrevProof(prev_proof) => { println!("verify header chain of prev proof"); - let groth16_vk = *IMM_GROTH16_VK_BYTES; - let zkm_vk_hash = String::from_utf8(input.zkm_vk_hash.to_vec()).unwrap(); - Groth16Verifier::verify_by_imm_groth16_vk( + verifier::verify_groth16_proof( &input.zkm_proof, &input.zkm_public_values, - &zkm_vk_hash, - groth16_vk, - ¤t_part_stark_vk, + &input.zkm_vk_hash, + &input.zkm_version, ) .unwrap(); @@ -39,5 +34,5 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui }; chain_state.apply_blocks(input.block_headers); - BlockHeaderCircuitOutput { chain_state, part_stark_vk: current_part_stark_vk } + BlockHeaderCircuitOutput { chain_state } } diff --git a/crates/state-chain/Cargo.toml b/crates/state-chain/Cargo.toml index c7a1aa50..8e98a570 100644 --- a/crates/state-chain/Cargo.toml +++ b/crates/state-chain/Cargo.toml @@ -9,11 +9,11 @@ bitcoin = { workspace = true, features = ["serde", "rand", "secp-recovery"] } bincode = "1.3.3" # workspace -guest-executor = { workspace = true } +guest-executor = { workspace = true } header-chain = { path = "../header-chain" } # Ziren -zkm-verifier = { workspace = true } +verifier = { workspace = true } zkm-zkvm = { workspace = true } #zkm-verifier = { path = "../../../Ziren/crates/verifier" } #zkm-zkvm = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } @@ -22,7 +22,7 @@ zkm-zkvm = { 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, features = ["serde", "bn"], default-features = false } sha2 = "0.10.9" @@ -39,7 +39,7 @@ serde_json = { workspace = true } cosmos-sdk-proto = { version = "0.27", default-features = false, features = ["serde"] } 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", ] } #pbjson = "0.8" @@ -47,7 +47,7 @@ tendermint-light-client-verifier = { git = "https://github.com/ProjectZKM/tender [dev-dependencies] rand = "0.8.5" serde_json = "1.0" -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/state-chain/src/lib.rs b/crates/state-chain/src/lib.rs index 49c959c5..0fe26513 100644 --- a/crates/state-chain/src/lib.rs +++ b/crates/state-chain/src/lib.rs @@ -3,10 +3,8 @@ mod state_chain; pub use cbft::*; pub use state_chain::*; -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOutput { - let current_part_stark_vk = Groth16Verifier::get_part_stark_vk(&input.zkm_version).to_vec(); let mut chain_state = match input.prev_proof { StateChainPrevProofType::GenesisBlock => { let block_hash: [u8; 32] = input.blocks[0].evm_block.current_block.hash_slow().into(); @@ -16,20 +14,18 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu } StateChainPrevProofType::PrevProof(prev_proof) => { println!("verify state chain of prev proof"); - let groth16_vk = *IMM_GROTH16_VK_BYTES; - let zkm_vk_hash = String::from_utf8(input.zkm_vk_hash.to_vec()).unwrap(); - Groth16Verifier::verify_by_imm_groth16_vk( + verifier::verify_groth16_proof( &input.zkm_proof, &input.zkm_public_values, - &zkm_vk_hash, - groth16_vk, - ¤t_part_stark_vk, + &input.zkm_vk_hash, + &input.zkm_version, ) .unwrap(); + prev_proof.chain_state } }; chain_state.apply_blocks(input.blocks); - StateChainCircuitOutput { chain_state, part_stark_vk: current_part_stark_vk } + StateChainCircuitOutput { chain_state } } diff --git a/crates/state-chain/src/state_chain.rs b/crates/state-chain/src/state_chain.rs index ce735638..473db11e 100644 --- a/crates/state-chain/src/state_chain.rs +++ b/crates/state-chain/src/state_chain.rs @@ -42,7 +42,6 @@ pub struct StateChainState { #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct StateChainCircuitOutput { pub chain_state: StateChainState, - pub part_stark_vk: Vec, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] diff --git a/crates/store/.sqlx/query-a574aa126ca49083135ef445cb213379bf8df51054958887f27081ff7f8f47cc.json b/crates/store/.sqlx/query-a574aa126ca49083135ef445cb213379bf8df51054958887f27081ff7f8f47cc.json deleted file mode 100644 index a5cebc2d..00000000 --- a/crates/store/.sqlx/query-a574aa126ca49083135ef445cb213379bf8df51054958887f27081ff7f8f47cc.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT OR\n REPLACE INTO graph (graph_id, instance_id, kickoff_index, from_addr, to_addr, amount, challenge_amount,\n status, sub_status, operator_pubkey, cur_prekickoff_txid, next_prekickoff, force_skip_kickoff_txid,\n quick_challenge_txid, challenge_incomplete_kickoff_txid, pegin_txid, kickoff_txid, take1_txid,\n challenge_txid, take2_txid, disprove_txid, watchtower_challenge_init_txid, watchtower_challenge_timeout_txids, nack_txids,\n blockhash_commit_timeout_txid, assert_init_txid, assert_commit_timeout_txids, init_withdraw_tx_hash,\n bridge_out_start_at, status_updated_at, proceed_withdraw_height, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - "describe": { - "columns": [], - "parameters": { - "Right": 33 - }, - "nullable": [] - }, - "hash": "a574aa126ca49083135ef445cb213379bf8df51054958887f27081ff7f8f47cc" -} diff --git a/crates/store/.sqlx/query-cc6525abb198055f787dd0bfcc0d0095ecb645fdfd91d202ce6aba0220349737.json b/crates/store/.sqlx/query-cc6525abb198055f787dd0bfcc0d0095ecb645fdfd91d202ce6aba0220349737.json new file mode 100644 index 00000000..f9b16ce8 --- /dev/null +++ b/crates/store/.sqlx/query-cc6525abb198055f787dd0bfcc0d0095ecb645fdfd91d202ce6aba0220349737.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT OR\n REPLACE INTO graph (graph_id, instance_id, kickoff_index, from_addr, to_addr, amount, challenge_amount,\n status, sub_status, operator_pubkey, cur_prekickoff_txid, next_prekickoff, force_skip_kickoff_txid,\n quick_challenge_txid, challenge_incomplete_kickoff_txid, pegin_txid, kickoff_txid, take1_txid,\n challenge_txid, take2_txid, watchtower_challenge_init_txid, operator_assert_txid, verifier_assert_txids, disprove_txids,\n watchtower_challenge_timeout_txids, operator_challenge_nack_txids, operator_commit_timeout_txid,\n init_withdraw_tx_hash,\n bridge_out_start_at, status_updated_at, proceed_withdraw_height, created_at, updated_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 33 + }, + "nullable": [] + }, + "hash": "cc6525abb198055f787dd0bfcc0d0095ecb645fdfd91d202ce6aba0220349737" +} diff --git a/crates/store/migrations/20250814113519_create_graph_table.sql b/crates/store/migrations/20250814113519_create_graph_table.sql index 94000307..7289917f 100644 --- a/crates/store/migrations/20250814113519_create_graph_table.sql +++ b/crates/store/migrations/20250814113519_create_graph_table.sql @@ -23,13 +23,10 @@ CREATE TABLE graph `take1_txid` TEXT, `challenge_txid` TEXT, `take2_txid` TEXT, - `disprove_txid` TEXT, `watchtower_challenge_init_txid` TEXT, - `watchtower_challenge_timeout_txids` TEXT NOT NULL DEFAULT '[]', - `nack_txids` TEXT NOT NULL DEFAULT '[]', - `blockhash_commit_timeout_txid` TEXT, - `assert_init_txid` TEXT, - `assert_commit_timeout_txids` TEXT NOT NULL DEFAULT '[]', + `operator_assert_txid` TEXT, + `verifier_assert_txids` TEXT NOT NULL DEFAULT '[]', + `disprove_txids` TEXT NOT NULL DEFAULT '[]', `init_withdraw_tx_hash` TEXT, `bridge_out_start_at` BIGINT NOT NULL DEFAULT 0, `zkm_version` TEXT NOT NULL DEFAULT '', diff --git a/crates/store/migrations/20260504120000_create_wrapper_proof_table.sql b/crates/store/migrations/20260504120000_create_wrapper_proof_table.sql new file mode 100644 index 00000000..6cd2adce --- /dev/null +++ b/crates/store/migrations/20260504120000_create_wrapper_proof_table.sql @@ -0,0 +1,30 @@ +CREATE TABLE IF NOT EXISTS wrapper_proof +( + `id` INTEGER PRIMARY KEY, + `operator_proof_id` BIGINT NOT NULL, + `instance_id` TEXT NOT NULL, + `graph_id` TEXT NOT NULL, + `execution_layer_block_number` BIGINT NOT NULL DEFAULT 0, + `operator_path_to_proof` TEXT NOT NULL DEFAULT '', + `path_to_proof` TEXT, + `public_value_hex` TEXT, + `operator_vk_hash` TEXT NOT NULL DEFAULT '', + `genesis_sequencer_commit_txid` TEXT NOT NULL DEFAULT '', + `operator_public_value_hex` TEXT, + `proof_size` BIGINT NOT NULL DEFAULT 0, + `cycles` BIGINT NOT NULL DEFAULT 0, + `proof_state` BIGINT NOT NULL DEFAULT 0 CHECK (proof_state IN (0, 1, 2, 3)), + `total_time_to_proof` BIGINT NOT NULL DEFAULT 0, + `proving_time` BIGINT NOT NULL DEFAULT 0, + `zkm_version` TEXT NOT NULL DEFAULT '', + `extra` TEXT, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + UNIQUE (`operator_proof_id`) +); + +CREATE INDEX IF NOT EXISTS idx_wrapper_proof_instance_graph + ON wrapper_proof (`instance_id`, `graph_id`); + +CREATE INDEX IF NOT EXISTS idx_wrapper_proof_state + ON wrapper_proof (`proof_state`); diff --git a/crates/store/migrations/20260524000000_create_pending_graph_init_table.sql b/crates/store/migrations/20260524000000_create_pending_graph_init_table.sql new file mode 100644 index 00000000..f03cb8c0 --- /dev/null +++ b/crates/store/migrations/20260524000000_create_pending_graph_init_table.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS pending_graph_init +( + `instance_id` TEXT NOT NULL, + `operator_pubkey` TEXT NOT NULL, + `graph_id` TEXT NOT NULL UNIQUE, + `created_at` BIGINT NOT NULL DEFAULT 0, + `updated_at` BIGINT NOT NULL DEFAULT 0, + PRIMARY KEY (`instance_id`, `operator_pubkey`) +); diff --git a/crates/store/migrations/20260611000000_add_gc_v2_graph_txids.sql b/crates/store/migrations/20260611000000_add_gc_v2_graph_txids.sql new file mode 100644 index 00000000..5f468bd6 --- /dev/null +++ b/crates/store/migrations/20260611000000_add_gc_v2_graph_txids.sql @@ -0,0 +1,8 @@ +ALTER TABLE graph + ADD COLUMN `watchtower_challenge_timeout_txids` TEXT NOT NULL DEFAULT '[]'; + +ALTER TABLE graph + ADD COLUMN `operator_challenge_nack_txids` TEXT NOT NULL DEFAULT '[]'; + +ALTER TABLE graph + ADD COLUMN `operator_commit_timeout_txid` TEXT; diff --git a/crates/store/src/localdb.rs b/crates/store/src/localdb.rs index d80ad4b0..c6e0e590 100644 --- a/crates/store/src/localdb.rs +++ b/crates/store/src/localdb.rs @@ -2,8 +2,8 @@ use crate::utils::{QueryBuilder, QueryParam, create_place_holders}; use crate::{ BridgeOutGlobalStats, GoatTxRecord, Graph, GraphBtcTxVoutMonitor, GraphRawData, Instance, LongRunningTaskProof, Message, Node, NodesOverview, OperatorProof, PeginGraphProcessData, - PeginInstanceProcessData, SequencerSetHashChange, SequencerSetScanState, SerializableTxid, - WatchContract, WatchtowerProof, + PeginInstanceProcessData, PendingGraphInit, ProofState, SequencerSetHashChange, + SequencerSetScanState, SerializableTxid, WatchContract, WatchtowerProof, WrapperProof, }; use indexmap::IndexMap; @@ -503,7 +503,10 @@ pub struct GraphUpdate { pub status: Option, pub sub_status: Option, pub challenge_txid: Option, - pub disprove_txid: Option, + pub disprove_txids: Option>, + pub watchtower_challenge_timeout_txids: Option>, + pub operator_challenge_nack_txids: Option>, + pub operator_commit_timeout_txid: Option>, pub bridge_out_start_at: Option, pub init_withdraw_tx_hash: Option, pub proceed_withdraw_height: Option, @@ -517,7 +520,10 @@ impl GraphUpdate { status: None, sub_status: None, challenge_txid: None, - disprove_txid: None, + disprove_txids: None, + watchtower_challenge_timeout_txids: None, + operator_challenge_nack_txids: None, + operator_commit_timeout_txid: None, bridge_out_start_at: None, init_withdraw_tx_hash: None, proceed_withdraw_height: None, @@ -541,9 +547,36 @@ impl GraphUpdate { self } - /// Set disprove transaction ID - pub fn with_disprove_txid(mut self, disprove_txid: SerializableTxid) -> Self { - self.disprove_txid = Some(disprove_txid); + /// Set disprove transaction IDs + pub fn with_disprove_txids(mut self, disprove_txids: Vec) -> Self { + self.disprove_txids = Some(disprove_txids); + self + } + + /// Set watchtower challenge timeout transaction IDs + pub fn with_watchtower_challenge_timeout_txids( + mut self, + watchtower_challenge_timeout_txids: Vec, + ) -> Self { + self.watchtower_challenge_timeout_txids = Some(watchtower_challenge_timeout_txids); + self + } + + /// Set operator challenge NACK transaction IDs + pub fn with_operator_challenge_nack_txids( + mut self, + operator_challenge_nack_txids: Vec, + ) -> Self { + self.operator_challenge_nack_txids = Some(operator_challenge_nack_txids); + self + } + + /// Set operator commit timeout transaction ID + pub fn with_operator_commit_timeout_txid( + mut self, + operator_commit_timeout_txid: Option, + ) -> Self { + self.operator_commit_timeout_txid = Some(operator_commit_timeout_txid); self } @@ -570,7 +603,10 @@ impl GraphUpdate { self.status.is_some() || self.sub_status.is_some() || self.challenge_txid.is_some() - || self.disprove_txid.is_some() + || self.disprove_txids.is_some() + || self.watchtower_challenge_timeout_txids.is_some() + || self.operator_challenge_nack_txids.is_some() + || self.operator_commit_timeout_txid.is_some() || self.bridge_out_start_at.is_some() || self.init_withdraw_tx_hash.is_some() || self.proceed_withdraw_height.is_some() @@ -590,8 +626,39 @@ impl GraphUpdate { if let Some(ref challenge_txid) = self.challenge_txid { query_builder.set_field("challenge_txid", QueryParam::BTCTxid(challenge_txid.clone())); } - if let Some(ref disprove_txid) = self.disprove_txid { - query_builder.set_field("disprove_txid", QueryParam::BTCTxid(disprove_txid.clone())); + if let Some(ref disprove_txids) = self.disprove_txids { + let disprove_txids_json = + serde_json::to_string(disprove_txids).unwrap_or_else(|_| "[]".into()); + query_builder.set_field("disprove_txids", QueryParam::Text(disprove_txids_json)); + } + if let Some(ref watchtower_challenge_timeout_txids) = + self.watchtower_challenge_timeout_txids + { + let watchtower_challenge_timeout_txids_json = + serde_json::to_string(watchtower_challenge_timeout_txids) + .unwrap_or_else(|_| "[]".into()); + query_builder.set_field( + "watchtower_challenge_timeout_txids", + QueryParam::Text(watchtower_challenge_timeout_txids_json), + ); + } + if let Some(ref operator_challenge_nack_txids) = self.operator_challenge_nack_txids { + let operator_challenge_nack_txids_json = + serde_json::to_string(operator_challenge_nack_txids) + .unwrap_or_else(|_| "[]".into()); + query_builder.set_field( + "operator_challenge_nack_txids", + QueryParam::Text(operator_challenge_nack_txids_json), + ); + } + if let Some(ref operator_commit_timeout_txid) = self.operator_commit_timeout_txid { + match operator_commit_timeout_txid { + Some(operator_commit_timeout_txid) => query_builder.set_field( + "operator_commit_timeout_txid", + QueryParam::BTCTxid(operator_commit_timeout_txid.clone()), + ), + None => query_builder.set_field_null("operator_commit_timeout_txid"), + } } if let Some(bridge_out_start_at) = self.bridge_out_start_at { query_builder.set_field("bridge_out_start_at", QueryParam::Int(bridge_out_start_at)); @@ -1082,6 +1149,76 @@ impl<'a> StorageProcessor<'a> { Ok(res.and_then(|record| record.parameters)) } + pub async fn upsert_pending_graph_init( + &mut self, + instance_id: &Uuid, + operator_pubkey: &str, + graph_id: &Uuid, + ) -> anyhow::Result { + let current_time = get_current_timestamp_secs(); + let result = sqlx::query( + "INSERT INTO pending_graph_init + (instance_id, operator_pubkey, graph_id, created_at, updated_at) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(instance_id, operator_pubkey) DO UPDATE SET + graph_id = excluded.graph_id, + updated_at = excluded.updated_at", + ) + .bind(instance_id) + .bind(operator_pubkey) + .bind(graph_id) + .bind(current_time) + .bind(current_time) + .execute(self.conn()) + .await?; + Ok(result.rows_affected()) + } + + pub async fn find_pending_graph_init_by_instance_and_operator_pubkey( + &mut self, + instance_id: &Uuid, + operator_pubkey: &str, + ) -> anyhow::Result> { + Ok(sqlx::query_as::<_, PendingGraphInit>( + "SELECT instance_id, operator_pubkey, graph_id, created_at, updated_at + FROM pending_graph_init + WHERE instance_id = ? AND operator_pubkey = ?", + ) + .bind(instance_id) + .bind(operator_pubkey) + .fetch_optional(self.conn()) + .await?) + } + + pub async fn find_pending_graph_init_by_graph_id( + &mut self, + graph_id: &Uuid, + ) -> anyhow::Result> { + Ok(sqlx::query_as::<_, PendingGraphInit>( + "SELECT instance_id, operator_pubkey, graph_id, created_at, updated_at + FROM pending_graph_init + WHERE graph_id = ?", + ) + .bind(graph_id) + .fetch_optional(self.conn()) + .await?) + } + + pub async fn delete_pending_graph_init( + &mut self, + instance_id: &Uuid, + operator_pubkey: &str, + ) -> anyhow::Result { + let result = sqlx::query( + "DELETE FROM pending_graph_init WHERE instance_id = ? AND operator_pubkey = ?", + ) + .bind(instance_id) + .bind(operator_pubkey) + .execute(self.conn()) + .await?; + Ok(result.rows_affected()) + } + /// Insert or update a graph /// /// Performs an INSERT OR REPLACE operation on the graph table. @@ -1095,18 +1232,20 @@ impl<'a> StorageProcessor<'a> { /// - Ok(affected_rows) number of rows affected by the operation /// - Err if the operation failed pub async fn upsert_graph(&mut self, graph: &Graph) -> anyhow::Result { - let nack_txids_json = serde_json::to_string(&graph.nack_txids)?; + let verifier_assert_txids_json = serde_json::to_string(&graph.verifier_assert_txids)?; + let disprove_txids_json = serde_json::to_string(&graph.disprove_txids)?; let watchtower_challenge_timeout_txids_json = serde_json::to_string(&graph.watchtower_challenge_timeout_txids)?; - let assert_commit_timeout_txids_json = - serde_json::to_string(&graph.assert_commit_timeout_txids)?; + let operator_challenge_nack_txids_json = + serde_json::to_string(&graph.operator_challenge_nack_txids)?; let res = sqlx::query!( "INSERT OR REPLACE INTO graph (graph_id, instance_id, kickoff_index, from_addr, to_addr, amount, challenge_amount, status, sub_status, operator_pubkey, cur_prekickoff_txid, next_prekickoff, force_skip_kickoff_txid, quick_challenge_txid, challenge_incomplete_kickoff_txid, pegin_txid, kickoff_txid, take1_txid, - challenge_txid, take2_txid, disprove_txid, watchtower_challenge_init_txid, watchtower_challenge_timeout_txids, nack_txids, - blockhash_commit_timeout_txid, assert_init_txid, assert_commit_timeout_txids, init_withdraw_tx_hash, + challenge_txid, take2_txid, watchtower_challenge_init_txid, operator_assert_txid, verifier_assert_txids, disprove_txids, + watchtower_challenge_timeout_txids, operator_challenge_nack_txids, operator_commit_timeout_txid, + init_withdraw_tx_hash, bridge_out_start_at, status_updated_at, proceed_withdraw_height, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", graph.graph_id, @@ -1129,21 +1268,22 @@ impl<'a> StorageProcessor<'a> { graph.take1_txid, graph.challenge_txid, graph.take2_txid, - graph.disprove_txid, graph.watchtower_challenge_init_txid, + graph.operator_assert_txid, + verifier_assert_txids_json, + disprove_txids_json, watchtower_challenge_timeout_txids_json, - nack_txids_json, - graph.blockhash_commit_timeout_txid, - graph.assert_init_txid, - assert_commit_timeout_txids_json, + operator_challenge_nack_txids_json, + graph.operator_commit_timeout_txid, graph.init_withdraw_tx_hash, graph.bridge_out_start_at, graph.status_updated_at, graph.proceed_withdraw_height, graph.created_at, graph.updated_at, - ).execute(self.conn()) - .await?; + ) + .execute(self.conn()) + .await?; Ok(res.rows_affected()) } @@ -1212,13 +1352,13 @@ impl<'a> StorageProcessor<'a> { take1_txid, challenge_txid, take2_txid, - disprove_txid, watchtower_challenge_init_txid, + operator_assert_txid, + verifier_assert_txids, + disprove_txids, watchtower_challenge_timeout_txids, - nack_txids, - blockhash_commit_timeout_txid, - assert_init_txid, - assert_commit_timeout_txids, + operator_challenge_nack_txids, + operator_commit_timeout_txid, init_withdraw_tx_hash, bridge_out_start_at, status_updated_at, @@ -1555,9 +1695,9 @@ impl<'a> StorageProcessor<'a> { for record in records { res.total += record.total; match record.actor.as_str() { - "Challenger" => { - (res.offline_challengers, res.online_challengers) = - (record.offline, record.online); + "Verifier" => { + res.offline_verifiers += record.offline; + res.online_verifiers += record.online; } "Operator" => { (res.offline_operators, res.online_operators) = (record.offline, record.online); @@ -2678,6 +2818,223 @@ impl<'a> StorageProcessor<'a> { Ok(res) } + pub async fn create_wrapper_proof( + &mut self, + wrapper_proof: &WrapperProof, + ) -> anyhow::Result { + let res = sqlx::query( + "INSERT INTO wrapper_proof ( + operator_proof_id, + instance_id, + graph_id, + execution_layer_block_number, + operator_path_to_proof, + path_to_proof, + public_value_hex, + operator_vk_hash, + genesis_sequencer_commit_txid, + operator_public_value_hex, + proof_size, + cycles, + proof_state, + total_time_to_proof, + proving_time, + zkm_version, + extra, + created_at, + updated_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + ) + .bind(wrapper_proof.operator_proof_id) + .bind(wrapper_proof.instance_id) + .bind(wrapper_proof.graph_id) + .bind(wrapper_proof.execution_layer_block_number) + .bind(&wrapper_proof.operator_path_to_proof) + .bind(&wrapper_proof.path_to_proof) + .bind(&wrapper_proof.public_value_hex) + .bind(&wrapper_proof.operator_vk_hash) + .bind(&wrapper_proof.genesis_sequencer_commit_txid) + .bind(&wrapper_proof.operator_public_value_hex) + .bind(wrapper_proof.proof_size) + .bind(wrapper_proof.cycles) + .bind(wrapper_proof.proof_state) + .bind(wrapper_proof.total_time_to_proof) + .bind(wrapper_proof.proving_time) + .bind(&wrapper_proof.zkm_version) + .bind(&wrapper_proof.extra) + .bind(wrapper_proof.created_at) + .bind(wrapper_proof.updated_at) + .execute(self.conn()) + .await?; + Ok(res.rows_affected()) + } + + pub async fn find_wrapper_proof_by_operator_proof_id( + &mut self, + operator_proof_id: i64, + ) -> anyhow::Result> { + let res = sqlx::query_as::<_, WrapperProof>( + "SELECT * FROM wrapper_proof + WHERE operator_proof_id = ? + LIMIT 1", + ) + .bind(operator_proof_id) + .fetch_optional(self.conn()) + .await?; + Ok(res) + } + + pub async fn find_wrapper_proof_by_instance_and_graph( + &mut self, + instance_id: &Uuid, + graph_id: &Uuid, + ) -> anyhow::Result> { + let res = sqlx::query_as::<_, WrapperProof>( + "SELECT * FROM wrapper_proof + WHERE instance_id = ? + AND graph_id = ? + ORDER BY id DESC + LIMIT 1", + ) + .bind(instance_id) + .bind(graph_id) + .fetch_optional(self.conn()) + .await?; + Ok(res) + } + + pub async fn find_next_wrapper_proof(&mut self) -> anyhow::Result> { + let res = sqlx::query_as::<_, WrapperProof>( + "SELECT * FROM wrapper_proof + WHERE proof_state = ? + ORDER BY id ASC + LIMIT 1", + ) + .bind(ProofState::New.to_i64()) + .fetch_optional(self.conn()) + .await?; + Ok(res) + } + + pub async fn claim_next_wrapper_proof(&mut self) -> anyhow::Result> { + let Some(candidate) = self.find_next_wrapper_proof().await? else { + return Ok(None); + }; + + let current_time = get_current_timestamp_secs(); + let updated = sqlx::query( + "UPDATE wrapper_proof + SET proof_state = ?, + updated_at = ? + WHERE id = ? + AND proof_state = ?", + ) + .bind(ProofState::Proving.to_i64()) + .bind(current_time) + .bind(candidate.id) + .bind(ProofState::New.to_i64()) + .execute(self.conn()) + .await? + .rows_affected(); + + if updated == 0 { + return Ok(None); + } + + let res = sqlx::query_as::<_, WrapperProof>( + "SELECT * FROM wrapper_proof + WHERE id = ? + LIMIT 1", + ) + .bind(candidate.id) + .fetch_optional(self.conn()) + .await?; + Ok(res) + } + + pub async fn find_proven_operator_proofs_without_wrapper( + &mut self, + limit: i64, + ) -> anyhow::Result> { + let res = sqlx::query_as::<_, OperatorProof>( + "SELECT operator_proof.* + FROM operator_proof + WHERE operator_proof.proof_state = ? + AND operator_proof.path_to_proof IS NOT NULL + AND NOT EXISTS ( + SELECT 1 + FROM wrapper_proof + WHERE wrapper_proof.operator_proof_id = operator_proof.id + ) + ORDER BY operator_proof.id ASC + LIMIT ?", + ) + .bind(ProofState::Proven.to_i64()) + .bind(limit) + .fetch_all(self.conn()) + .await?; + Ok(res) + } + + #[allow(clippy::too_many_arguments)] + pub async fn update_wrapper_proof_success( + &mut self, + id: i64, + path_to_proof: String, + public_value_hex: String, + proof_size: i64, + cycles: i64, + proving_time: i64, + zkm_version: &str, + ) -> anyhow::Result { + let current_time = get_current_timestamp_secs(); + let res = sqlx::query( + "UPDATE wrapper_proof + SET path_to_proof = ?, + public_value_hex = ?, + proof_size = ?, + cycles = ?, + proof_state = ?, + total_time_to_proof = CASE + WHEN created_at > 0 THEN (? - created_at) * 1000 + ELSE total_time_to_proof + END, + proving_time = ?, + zkm_version = ?, + updated_at = ? + WHERE id = ?", + ) + .bind(path_to_proof) + .bind(public_value_hex) + .bind(proof_size) + .bind(cycles) + .bind(ProofState::Proven.to_i64()) + .bind(current_time) + .bind(proving_time) + .bind(zkm_version) + .bind(current_time) + .bind(id) + .execute(self.conn()) + .await?; + Ok(res.rows_affected()) + } + + pub async fn update_wrapper_proof_failure(&mut self, id: i64) -> anyhow::Result { + let current_time = get_current_timestamp_secs(); + let res = sqlx::query( + "UPDATE wrapper_proof + SET proof_state = ?, + updated_at = ? + WHERE id = ?", + ) + .bind(ProofState::Failed.to_i64()) + .bind(current_time) + .bind(id) + .execute(self.conn()) + .await?; + Ok(res.rows_affected()) + } + #[allow(clippy::too_many_arguments)] pub async fn update_watchtower_proof( &mut self, @@ -2785,7 +3142,8 @@ impl<'a> StorageProcessor<'a> { "SELECT * FROM watchtower_proof WHERE instance_id = ? - AND graph_id = ?", + AND graph_id = ? + ORDER BY node_index ASC", ) .bind(instance_id) .bind(graph_id) @@ -2944,12 +3302,12 @@ impl<'a> StorageProcessor<'a> { Ok(res) } - pub async fn find_first_sequencer_set_hash_change_by_goat_block_at_or_after( + pub async fn find_first_sequencer_set_hash_change_by_goat_block_at_or_before( &mut self, goat_block_height: i64, ) -> anyhow::Result> { let res = sqlx::query_as::<_, SequencerSetHashChange>( - "SELECT * FROM sequencer_set_hash_changes WHERE goat_block_height >= ? ORDER BY goat_block_height ASC LIMIT 1", + "SELECT * FROM sequencer_set_hash_changes WHERE goat_block_height <= ? ORDER BY goat_block_height DESC LIMIT 1", ) .bind(goat_block_height) .fetch_optional(self.conn()) @@ -2957,19 +3315,6 @@ impl<'a> StorageProcessor<'a> { Ok(res) } - pub async fn find_first_sequencer_set_hash_change_by_cosmos_block_at_or_after( - &mut self, - cosmos_block_height: i64, - ) -> anyhow::Result> { - let res = sqlx::query_as::<_, SequencerSetHashChange>( - "SELECT * FROM sequencer_set_hash_changes WHERE cosmos_block_height >= ? ORDER BY cosmos_block_height ASC LIMIT 1", - ) - .bind(cosmos_block_height) - .fetch_optional(self.conn()) - .await?; - Ok(res) - } - pub async fn get_sequencer_set_scan_state( &mut self, ) -> anyhow::Result> { @@ -3057,7 +3402,7 @@ mod sequencer_set_tests { } #[tokio::test] - async fn test_find_by_goat_block_at_or_after() { + async fn test_find_by_goat_block_at_or_before() { let db = setup_db().await; let mut s = db.acquire().await.unwrap(); @@ -3067,43 +3412,25 @@ mod sequencer_set_tests { // Exact match let r = s - .find_first_sequencer_set_hash_change_by_goat_block_at_or_after(2000) + .find_first_sequencer_set_hash_change_by_goat_block_at_or_before(2000) .await .unwrap() .unwrap(); assert_eq!(r.goat_block_height, 2000); + assert_eq!(r.validators_hash, "bb"); - // Between records — should return next one + // Between records — should return previous one let r = s - .find_first_sequencer_set_hash_change_by_goat_block_at_or_after(1500) + .find_first_sequencer_set_hash_change_by_goat_block_at_or_before(2500) .await .unwrap() .unwrap(); assert_eq!(r.goat_block_height, 2000); + assert_eq!(r.validators_hash, "bb"); - // Beyond all records — should return None - let r = - s.find_first_sequencer_set_hash_change_by_goat_block_at_or_after(4000).await.unwrap(); - assert!(r.is_none()); - } - - #[tokio::test] - async fn test_find_by_cosmos_block_at_or_after() { - let db = setup_db().await; - let mut s = db.acquire().await.unwrap(); - - s.upsert_sequencer_set_hash_change(100, 1000, "aa").await.unwrap(); - s.upsert_sequencer_set_hash_change(200, 2000, "bb").await.unwrap(); - - let r = s - .find_first_sequencer_set_hash_change_by_cosmos_block_at_or_after(150) - .await - .unwrap() - .unwrap(); - assert_eq!(r.cosmos_block_height, 200); - + // Before all records — should return None let r = - s.find_first_sequencer_set_hash_change_by_cosmos_block_at_or_after(300).await.unwrap(); + s.find_first_sequencer_set_hash_change_by_goat_block_at_or_before(500).await.unwrap(); assert!(r.is_none()); } @@ -3128,23 +3455,77 @@ mod sequencer_set_tests { } #[tokio::test] - async fn test_find_returns_none_when_empty() { + async fn test_wrapper_proof_lifecycle() { let db = setup_db().await; let mut s = db.acquire().await.unwrap(); + let instance_id = Uuid::parse_str("00112233445566778899aabbccddeeff").unwrap(); + let graph_id = Uuid::parse_str("ffeeddccbbaa99887766554433221100").unwrap(); + let now = get_current_timestamp_secs(); - assert!(s.find_latest_sequencer_set_hash_change().await.unwrap().is_none()); - assert!( - s.find_first_sequencer_set_hash_change_by_goat_block_at_or_after(0) - .await - .unwrap() - .is_none() - ); - assert!( - s.find_first_sequencer_set_hash_change_by_cosmos_block_at_or_after(0) - .await - .unwrap() - .is_none() - ); - assert!(s.get_sequencer_set_scan_state().await.unwrap().is_none()); + let task = WrapperProof { + operator_proof_id: 42, + instance_id, + graph_id, + execution_layer_block_number: 9511055, + operator_path_to_proof: "operator.bin".to_string(), + operator_vk_hash: "operator-vk-hash".to_string(), + genesis_sequencer_commit_txid: + "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246".to_string(), + operator_public_value_hex: Some("abcd".to_string()), + proof_state: ProofState::New.to_i64(), + created_at: now, + updated_at: now, + ..Default::default() + }; + + s.create_wrapper_proof(&task).await.unwrap(); + let duplicate = s.create_wrapper_proof(&task).await; + assert!(duplicate.is_err(), "operator_proof_id must be unique"); + + let found = s + .find_wrapper_proof_by_operator_proof_id(42) + .await + .unwrap() + .expect("wrapper proof should be found"); + assert_eq!(found.instance_id, instance_id); + assert_eq!(found.graph_id, graph_id); + assert_eq!(found.proof_state, ProofState::New.to_i64()); + + let claimed = s + .claim_next_wrapper_proof() + .await + .unwrap() + .expect("new wrapper proof should be claimable"); + assert_eq!(claimed.id, found.id); + assert_eq!(claimed.proof_state, ProofState::Proving.to_i64()); + + s.update_wrapper_proof_success( + claimed.id, + "wrapper.bin".to_string(), + "wrapper-public-value-hex".to_string(), + 1234, + 5678, + 90, + "v1.2.5", + ) + .await + .unwrap(); + let proven = s + .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) + .await + .unwrap() + .expect("wrapper proof should be found by instance and graph"); + assert_eq!(proven.proof_state, ProofState::Proven.to_i64()); + assert_eq!(proven.path_to_proof.as_deref(), Some("wrapper.bin")); + assert_eq!(proven.public_value_hex.as_deref(), Some("wrapper-public-value-hex")); + + s.update_wrapper_proof_failure(proven.id).await.unwrap(); + let failed = s + .find_wrapper_proof_by_operator_proof_id(42) + .await + .unwrap() + .expect("wrapper proof should still exist"); + assert_eq!(failed.proof_state, ProofState::Failed.to_i64()); + assert!(s.claim_next_wrapper_proof().await.unwrap().is_none()); } } diff --git a/crates/store/src/schema.rs b/crates/store/src/schema.rs index c40da0cd..509ebb1f 100644 --- a/crates/store/src/schema.rs +++ b/crates/store/src/schema.rs @@ -176,8 +176,8 @@ pub struct NodesOverview { pub total: i64, pub online_operators: i64, pub offline_operators: i64, - pub online_challengers: i64, - pub offline_challengers: i64, + pub online_verifiers: i64, + pub offline_verifiers: i64, pub online_committees: i64, pub offline_committees: i64, pub online_watchtowers: i64, @@ -396,16 +396,17 @@ pub struct Graph { pub take1_txid: Option, pub challenge_txid: Option, pub take2_txid: Option, - pub disprove_txid: Option, pub watchtower_challenge_init_txid: Option, + pub operator_assert_txid: Option, #[sqlx(json)] - pub watchtower_challenge_timeout_txids: Vec, + pub verifier_assert_txids: Vec, + #[sqlx(json)] + pub disprove_txids: Vec, #[sqlx(json)] - pub nack_txids: Vec, - pub blockhash_commit_timeout_txid: Option, - pub assert_init_txid: Option, + pub watchtower_challenge_timeout_txids: Vec, #[sqlx(json)] - pub assert_commit_timeout_txids: Vec, + pub operator_challenge_nack_txids: Vec, + pub operator_commit_timeout_txid: Option, pub init_withdraw_tx_hash: Option, pub bridge_out_start_at: i64, pub status_updated_at: i64, @@ -467,12 +468,25 @@ pub struct PeginGraphProcessData { pub created_at: i64, } +#[derive(Clone, FromRow, Debug, Serialize, Deserialize, Default)] +pub struct PendingGraphInit { + pub instance_id: Uuid, + pub operator_pubkey: String, + pub graph_id: Uuid, + pub updated_at: i64, + pub created_at: i64, +} + #[derive(Debug, Clone, PartialEq, Display, EnumString)] pub enum MessageType { None, PeginRequest, CreateGraph, ConfirmInstance, + InitGraph, + GenCircuits, + CutCircuits, + SolderingProof, NonceGeneration, CommitteePresign, GraphFinalize, @@ -487,13 +501,13 @@ pub enum MessageType { WatchtowerChallengeInitSent, WatchtowerChallengeSent, WatchtowerChallengeTimeout, - OperatorAckTimeout, - OperatorCommitBlockHashReady, - OperatorCommitBlockHashSent, - OperatorCommitBlockHashTimeout, - AssertInitReady, - AssertCommitTimeout, - DisproveReady, + NackReady, + OperatorCommitPubinReady, + OperatorCommitPubinTimeout, + AssertReady, + AssertSent, + ChallengeAssertSent, + WronglyChallengeTimeout, DisproveSent, Take1Ready, Take1Sent, @@ -710,6 +724,30 @@ pub struct OperatorProof { pub updated_at: i64, } +#[derive(Clone, FromRow, Debug, Serialize, Deserialize, Default)] +pub struct WrapperProof { + pub id: i64, + pub operator_proof_id: i64, + pub instance_id: Uuid, + pub graph_id: Uuid, + pub execution_layer_block_number: i64, + pub operator_path_to_proof: String, + pub path_to_proof: Option, + pub public_value_hex: Option, + pub operator_vk_hash: String, + pub genesis_sequencer_commit_txid: String, + pub operator_public_value_hex: Option, + pub proof_size: i64, + pub cycles: i64, + pub proof_state: i64, + pub total_time_to_proof: i64, + pub proving_time: i64, + pub zkm_version: String, + pub extra: Option, + pub created_at: i64, + pub updated_at: i64, +} + #[derive(Clone, FromRow, Debug, Serialize, Deserialize, Default)] pub struct LongRunningTaskProof { pub block_start: i64, diff --git a/crates/verifier/Cargo.toml b/crates/verifier/Cargo.toml new file mode 100644 index 00000000..9201485e --- /dev/null +++ b/crates/verifier/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "verifier" +version.workspace = true +edition.workspace = true + +[dependencies] +zkm-verifier = { workspace = true } + +[lints] +workspace = true \ No newline at end of file diff --git a/crates/verifier/src/lib.rs b/crates/verifier/src/lib.rs new file mode 100644 index 00000000..e08fde4c --- /dev/null +++ b/crates/verifier/src/lib.rs @@ -0,0 +1,23 @@ +use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; + +pub fn verify_groth16_proof( + proof: &[u8], + zkm_public_values: &[u8], + zkm_vk_hash: &[u8], + zkm_version: &str, +) -> 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())?; + let part_stark_vk = Groth16Verifier::get_part_stark_vk(zkm_version); + + match Groth16Verifier::verify_by_imm_groth16_vk( + proof, + zkm_public_values, + &zkm_vk_hash, + groth16_vk, + part_stark_vk, + ) { + Ok(_) => Ok(()), + Err(err) => Err(format!("Verify Groth16 proof, err: {err:?}")), + } +} diff --git a/deployment/regtest/proof-builder-rpc/proof-builder.toml b/deployment/regtest/proof-builder-rpc/proof-builder.toml index 663663fc..6c050856 100644 --- a/deployment/regtest/proof-builder-rpc/proof-builder.toml +++ b/deployment/regtest/proof-builder-rpc/proof-builder.toml @@ -72,4 +72,10 @@ included_watchtowers = "1" graph_id="0x00112233445566778899aabbccddeeff" execution_layer_block_number = 9511055 index = 1 -bitcoin_network = "regtest" \ No newline at end of file +bitcoin_network = "regtest" + +[wrapper] +enable = false +output = "../circuits/data/wrapper/output.bin" +genesis_sequencer_commit_txid = "5d5a29e1c426abc4b947f90b93c6b043683aafad79cb3b9a9829dfd5e51cad09" +scan_interval = 30 diff --git a/docs/part-stark-vk-attest-cli.md b/docs/part-stark-vk-attest-cli.md deleted file mode 100644 index 73cb0bf9..00000000 --- a/docs/part-stark-vk-attest-cli.md +++ /dev/null @@ -1,281 +0,0 @@ -# `part-stark-vk-attest` CLI Guide - -This document explains how to use `part-stark-vk-attest` to build the latest `part_stark_vk` attestation snapshot and add publisher signatures to that snapshot. - -## Overview - -The CLI currently provides two subcommands: - -- `build-tree` -- `sign-root` - -The workflow is fixed: - -1. Run `build-tree` first to generate the latest snapshot from the full active version set -2. Let each publisher signer run `sign-root` once -3. After the number of signatures in `manifest.json` reaches the threshold from `commit_info`, the proof side can load the attestation snapshot - -## Binary Entry - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- ... -``` - -The binary is defined in: - -- [`node/Cargo.toml`](../node/Cargo.toml) -- [`node/src/bin/part_stark_vk_attest.rs`](../node/src/bin/part_stark_vk_attest.rs) - -## Directory Model - -The attestation directory stores only one latest snapshot. Historical snapshots are not kept. - -Default output directory: - -```text -data/psv-attestations -``` - -Latest snapshot layout: - -```text -data/psv-attestations/ - manifest.json - proofs/ - v1.2.4.json - v1.2.5.json -``` - -Where: - -- `manifest.json` stores the current Merkle root, ordered versions, publisher metadata, and aggregated signatures -- `proofs/.json` stores the `part_stark_vk`, `leaf_index`, and `merkle_path` for each version - -## Version Rules - -All `--versions` inputs represent the full active version set, not incremental additions. - -The CLI will: - -1. Sort versions by semver -2. Load `part_stark_vk` for each sorted version -3. Build the Merkle tree using that sorted order - -Example: - -```text -Input: v1.2.10,v1.2.4,v1.2.5 -Sorted: v1.2.4,v1.2.5,v1.2.10 -``` - -If versions are duplicated, the command fails. - -## `build-tree` - -### Purpose - -- Build the latest Merkle tree from the full version set -- Generate Merkle proofs for every version -- Overwrite the attestation directory -- Do not write signatures - -### Command Format - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - build-tree \ - --versions \ - --attestation-dir -``` - -### Parameters - -- `--versions` - Comma-separated version list -- `--attestation-dir` - Output attestation directory; defaults to `data/psv-attestations` - -### Example - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - build-tree \ - --versions v1.2.4,v1.2.5 \ - --attestation-dir circuits/data/psv-attestations -``` - -### Output - -On success, the command prints output similar to: - -```text -built latest part_stark_vk snapshot in circuits/data/psv-attestations for versions v1.2.4,v1.2.5 -``` - -Note: - -- `build-tree` creates an unsigned snapshot -- Running only `build-tree` is not enough for the proof side to consume the attestation - -## `sign-root` - -### Purpose - -- Rebuild the Merkle tree from the same version set -- Load the current publisher set and threshold from `commit_info.active_publisher_set()` -- Sign the current root with a single publisher private key -- Merge the signature into the latest `manifest.json` - -### Command Format - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - sign-root \ - --versions \ - --commit-info-file \ - --publisher-secret-key-wif \ - --attestation-dir -``` - -### Parameters - -- `--versions` - Full active version set; must match the snapshot being signed -- `--commit-info-file` - Path to `commit_info.json` -- `--publisher-secret-key-wif` - Bitcoin WIF private key for a single publisher signer -- `--attestation-dir` - Attestation directory; defaults to `data/psv-attestations` - -### Signer Matching Rule - -The command does not require a manual signer index. - -`--publisher-secret-key-wif` is parsed as WIF, following the same Bitcoin private key format used in `sequencer-set-publish.rs`. - -The CLI will: - -1. Derive the compressed public key from the private key -2. Read the current active publisher set from `commit_info.active_publisher_set()` -3. Find that public key inside the active publisher set -4. Use the matching position as `signer_pubkey_index` - -If the private key does not belong to the current active publisher set, the command fails. - -### Your Current `commit_info` - -If you are using: - -- [`circuits/data/commit-chain/commit_info.json.0`](../circuits/data/commit-chain/commit_info.json.0) - -Then the command looks like: - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - sign-root \ - --versions v1.2.4,v1.2.5 \ - --commit-info-file circuits/data/commit-chain/commit_info.json.0 \ - --publisher-secret-key-wif \ - --attestation-dir circuits/data/psv-attestations -``` - -### Multi-Signer Aggregation - -`sign-root` handles only one signer per invocation. - -If multiple signers exist, run it once per signer: - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - sign-root \ - --versions v1.2.4,v1.2.5 \ - --commit-info-file circuits/data/commit-chain/commit_info.json.0 \ - --publisher-secret-key-wif \ - --attestation-dir circuits/data/psv-attestations - -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - sign-root \ - --versions v1.2.4,v1.2.5 \ - --commit-info-file circuits/data/commit-chain/commit_info.json.0 \ - --publisher-secret-key-wif \ - --attestation-dir circuits/data/psv-attestations - -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - sign-root \ - --versions v1.2.4,v1.2.5 \ - --commit-info-file circuits/data/commit-chain/commit_info.json.0 \ - --publisher-secret-key-wif \ - --attestation-dir circuits/data/psv-attestations -``` - -Signatures are aggregated into the same `manifest.json`. - -## Signature Invalidation Rules - -Existing signatures are cleared if any of the following changes: - -- Version list changes -- Version ordering changes -- Any resolved `part_stark_vk` bytes change -- `root` changes -- `publisher_set_id` changes - -Notes: - -- `publisher_set_id` already includes `threshold` -- A threshold change therefore appears as a `publisher_set_id` change - -## How the Proof Side Uses It - -The proof side accepts only the latest snapshot format. - -Requirements: - -- `manifest.json` must exist in the attestation directory -- Matching `proofs/.json` files must exist -- `manifest.json` must already contain enough valid signatures - -If the directory contains only the old `bundles/` files and no `manifest.json`, the proof side fails immediately. - -## Recommended Procedure - -### Step 1: Build the Latest Snapshot - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - build-tree \ - --versions v1.2.4,v1.2.5 \ - --attestation-dir circuits/data/psv-attestations -``` - -### Step 2: Let Each Signer Add One Signature - -```bash -cargo run -p bitvm2-noded --bin part-stark-vk-attest -- \ - sign-root \ - --versions v1.2.4,v1.2.5 \ - --commit-info-file circuits/data/commit-chain/commit_info.json.0 \ - --publisher-secret-key-wif \ - --attestation-dir circuits/data/psv-attestations -``` - -Repeat until the number of signatures reaches the threshold from `commit_info`. - -## Common Errors - -- `versions is empty` - No version list was provided -- `duplicate version '...'` - Duplicate version after normalization -- `failed to load part_stark_vk for version '...'` - The local verifier cannot resolve that version -- `publisher_secret_key_wif does not belong to active publisher set` - The provided WIF key does not belong to the current active publisher set -- `missing latest attestation manifest '.../manifest.json'` - The proof side is loading an attestation directory without a latest snapshot - -## Relevant Implementation Files - -- [`node/src/bin/part_stark_vk_attest.rs`](../node/src/bin/part_stark_vk_attest.rs) -- [`crates/bitcoin-light-client-circuit/src/attestation.rs`](../crates/bitcoin-light-client-circuit/src/attestation.rs) diff --git a/node/Cargo.toml b/node/Cargo.toml index 9395478f..fa4949db 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -1,10 +1,10 @@ [package] -name = "bitvm2-noded" +name = "bitvm-noded" version.workspace = true edition.workspace = true [[bin]] -name = "bitvm2-noded" +name = "bitvm-noded" path = "src/main.rs" [[bin]] @@ -36,8 +36,12 @@ name = "update-db" path = "src/bin/db_inject.rs" [[bin]] -name = "part-stark-vk-attest" -path = "src/bin/part_stark_vk_attest.rs" +name = "fetch-watchtower-xonly-pubkeys" +path = "src/bin/fetch_watchtower_xonly_pubkeys.rs" + +[[bin]] +name = "mock-rpc" +path = "src/bin/mock_rpc.rs" [dependencies] libp2p = { workspace = true, features = [ @@ -84,6 +88,8 @@ tokio-util.workspace = true http-body-util.workspace = true axum = { workspace = true, features = ["macros", "json"] } reqwest = { workspace = true, features = ["json"] } +aws-config = { version = "1.1.9", features = ["behavior-version-latest"] } +aws-sdk-s3 = "1.21.0" tower-http = { version = "0.6.2", features = ["cors", "trace"] } http = { workspace = true } stun-client = "0.1.4" @@ -111,14 +117,17 @@ dirs = "5.0.1" zkm-sdk = { workspace = true } zkm-verifier = { workspace = true, features = ["ark"] } -borsh = {version = "1.5.3", features = ["derive"] } -semver = "1.0" +zkm-recursion-core = { workspace = true } +zkm-stark = { workspace = true } +p3-bn254-fr = { workspace = true } +p3-field = { workspace = true } +borsh = { version = "1.5.3", features = ["derive"] } proof-builder = { workspace = true } util = { workspace = true } bitvm = { workspace = true } goat = { workspace = true } -bitvm2-lib = { workspace = true } +bitvm-lib = { workspace = true } store = { workspace = true } client = { workspace = true } bitcoin-light-client-circuit = { workspace = true } diff --git a/node/README.md b/node/README.md index d165f3d5..9e65ac18 100644 --- a/node/README.md +++ b/node/README.md @@ -1,6 +1,6 @@ -# BitVM2 Node +# BitVM Node -The main node implementation for GOAT Network's BitVM2 bridge protocol. This module handles P2P networking, message processing, scheduled tasks, and RPC services for secure cross-chain asset transfers between Bitcoin and GOAT L2. +The main node implementation for GOAT Network's BitVM bridge protocol. This module handles P2P networking, message processing, scheduled tasks, and RPC services for secure cross-chain asset transfers between Bitcoin and GOAT L2. ## Table of Contents @@ -24,7 +24,7 @@ The main node implementation for GOAT Network's BitVM2 bridge protocol. This mod ## Overview -The BitVM2 Node (`bitvm2-noded`) is a multi-role distributed node that participates in the BitVM2 cross-chain bridge protocol. It enables trustless Bitcoin-to-L2 transfers through a combination of: +The BitVM Node (`bitvm-noded`) is a multi-role distributed node that participates in the BitVM cross-chain bridge protocol. It enables trustless Bitcoin-to-L2 transfers through a combination of: - **Multi-signature Consensus**: Committee-based transaction presigning - **Optimistic Verification**: Watchtower monitoring with dispute resolution @@ -56,7 +56,7 @@ flowchart TB PB["Proof Builder RPC"] end - subgraph Node["bitvm2-noded"] + subgraph Node["bitvm-noded"] subgraph Input["Input Layer"] EW["Event Watch Task"] P2P["P2P Swarm (libp2p)"] @@ -108,7 +108,7 @@ flowchart LR subgraph Actors["Actor Roles"] C["Committee"] O["Operator"] - CH["Challenger"] + V["Verifier"] W["Watchtower"] R["Relayer\n(Committee + Flag)"] end @@ -116,16 +116,16 @@ flowchart LR subgraph Network["P2P Topics"] TC["/goat/topic/Committee"] TO["/goat/topic/Operator"] - TCH["/goat/topic/Challenger"] + TV["/goat/topic/Verifier"] TW["/goat/topic/Watchtower"] TA["/goat/topic/All"] end C <--> TC O <--> TO - CH <--> TCH + V <--> TV W <--> TW - C & O & CH & W & R <--> TA + C & O & V & W & R <--> TA R -.->|"SyncGraph"| TA ``` @@ -135,7 +135,7 @@ flowchart LR ## Actor System -BitVM2 employs four primary actor roles plus an optional relayer capability: +BitVM employs four primary actor roles plus an optional relayer capability: ### Fig-02-1-Actor-Roles @@ -145,7 +145,7 @@ classDiagram <> Committee Operator - Challenger + Verifier Watchtower All } @@ -164,7 +164,7 @@ classDiagram +send_take1_take2() } - class Challenger { + class Verifier { +monitor_timeouts() +submit_disprove() } @@ -183,7 +183,7 @@ classDiagram Actor <|-- Committee Actor <|-- Operator - Actor <|-- Challenger + Actor <|-- Verifier Actor <|-- Watchtower Committee <|-- Relayer : ENABLE_RELAYER=true ``` @@ -196,7 +196,7 @@ classDiagram |------|------------------|--------------| | **Committee** | Multi-sig committee member, responsible for presigning and graph endorsement | `NonceGeneration`, `CommitteePresign`, `EndorseGraph` | | **Operator** | Bridge operator, creates graphs and executes withdrawal transactions | `CreateGraph`, `KickoffSent`, `Take1Sent`, `Take2Sent` | -| **Challenger** | Dispute challenger, monitors timeouts and submits disproofs | `DisproveReady`, `DisproveSent` | +| **Verifier** | Dispute verifier, monitors timeouts and submits disproofs | `DisproveReady`, `DisproveSent` | | **Watchtower** | Chain monitor, validates block headers and submits challenges | `WatchtowerChallengeSent` | | **Relayer** | Graph data distributor, responds to sync requests | `SyncGraphRequest`, `SyncGraph` | @@ -228,7 +228,7 @@ sequenceDiagram Committee->>Committee: Validate fees and availability Committee->>Operator: ConfirmInstance - Operator->>Operator: Create SimplifiedBitvm2Graph + Operator->>Operator: Create SimplifiedbitvmGraph Operator->>DB: Store graph_raw_data (JSON) Operator->>Committee: CreateGraph (with graph data) @@ -311,7 +311,7 @@ sequenceDiagram participant GOAT as GOAT L2 participant Operator participant Watchtower - participant Challenger + participant Verifier participant BTC as Bitcoin User->>GOAT: InitWithdraw @@ -327,7 +327,7 @@ sequenceDiagram Operator->>BTC: ACK response else Operator rejects Operator->>BTC: NACK response - Challenger->>BTC: DisproveTx + Verifier->>BTC: DisproveTx end end @@ -340,7 +340,7 @@ sequenceDiagram Operator->>BTC: Take2 transaction BTC->>User: BTC transferred to user address else Challenge failed - Challenger->>BTC: DisproveTx + Verifier->>BTC: DisproveTx Note over User,BTC: User funds safe, Operator penalized end ``` @@ -392,7 +392,7 @@ flowchart TB ## Graph State Machine -Graph is the core data structure in BitVM2, representing a set of presigned Bitcoin transactions. +Graph is the core data structure in BitVM, representing a set of presigned Bitcoin transactions. ### Fig-04-1-Graph-Lifecycle @@ -579,7 +579,7 @@ sequenceDiagram | Column | Type | Description | |--------|------|-------------| | `graph_id` | UUID | Primary key (FK to graph) | -| `raw_data` | TEXT | JSON-serialized SimplifiedBitvm2Graph | +| `raw_data` | TEXT | JSON-serialized SimplifiedbitvmGraph | | `created_at`, `updated_at` | i64 | Timestamps | --- @@ -622,7 +622,7 @@ classDiagram <> Committee Operator - Challenger + Verifier Watchtower All } @@ -665,9 +665,9 @@ classDiagram | `WatchtowerChallengeInitSent` | Operator | Watchtower | WT challenge initialization | | `WatchtowerChallengeSent` | Watchtower | Operator | WT challenge submission | | `WatchtowerChallengeTimeout` | System | Operator | WT challenge timeout | -| `OperatorAckTimeout` | System | Challenger | Operator ACK timeout | -| `DisproveReady` | System | Challenger | Disprove ready | -| `DisproveSent` | Challenger | All | Disprove transaction broadcast | +| `OperatorAckTimeout` | System | Verifier | Operator ACK timeout | +| `DisproveReady` | System | Verifier | Disprove ready | +| `DisproveSent` | Verifier | All | Disprove transaction broadcast | #### Synchronization Messages @@ -758,7 +758,7 @@ node/src/ ├── metrics_service.rs # Prometheus metrics ├── rpc_service/ # REST API implementation │ ├── mod.rs # Service orchestration -│ ├── bitvm2.rs # BitVM2-specific endpoints +│ ├── bitvm.rs # BitVM-specific endpoints │ ├── routes.rs # HTTP route definitions │ ├── validation.rs # Input validation │ └── handler/ # Request handlers @@ -793,20 +793,20 @@ BITCOIN_NETWORK=regtest cargo build -r BITCOIN_NETWORK=testnet4 cargo build -r # Build only the node binary -cargo build -r -p bitvm2-noded +cargo build -r -p bitvm-noded ``` ### Generate Node Keys ```bash # Generate P2P peer key -bitvm2-noded key peer +bitvm-noded key peer # Output: # PEER_KEY= # PEER_ID= -# Generate funding address (for Operator/Challenger) -bitvm2-noded key funding-address +# Generate funding address (for Operator/Verifier) +bitvm-noded key funding-address # Output: # Funding P2WSH address: bc1q... ``` @@ -814,19 +814,41 @@ bitvm2-noded key funding-address ### Start Node ```bash -bitvm2-noded \ +bitvm-noded \ --rpc-addr 0.0.0.0:8080 \ --db-path ./node.db \ --p2p-port 4001 \ --bootnodes /ip4/x.x.x.x/tcp/4001/p2p/ ``` +### Start Local Mock RPC + +Use this when you only need to test HTTP interfaces. It starts the RPC routes without +P2P, chain watchers, or maintenance tasks, and seeds a local SQLite database with +mock nodes, instances, graphs, and overview data. + +```bash +cargo run -p bitvm-noded --bin mock-rpc -- --rpc-addr 127.0.0.1:18080 +``` + +Useful test calls: + +```bash +curl http://127.0.0.1:18080/v1/nodes/overview +curl 'http://127.0.0.1:18080/v1/instances?is_bridge_in=true' +curl http://127.0.0.1:18080/v1/graphs +``` + +The mock binary prints seeded instance and graph IDs on startup. Endpoints that +require real `graph_raw_data`, such as graph transaction hex export, still need +real graph raw data in the database. + ### CLI Options | Option | Description | Default | |--------|-------------|---------| | `--rpc-addr` | RPC service bind address | `0.0.0.0:8080` | -| `--db-path` | SQLite database path | `sqlite:/tmp/bitvm2-node.db` | +| `--db-path` | SQLite database path | `sqlite:/tmp/bitvm-node.db` | | `--p2p-port` | P2P listen port | `0` (random) | | `--bootnodes` | Bootstrap node addresses | - | | `--metrics-path` | Prometheus metrics endpoint | `/metrics` | @@ -840,7 +862,7 @@ bitvm2-noded \ | Variable | Required | Description | Default | |----------|----------|-------------|---------| -| `ACTOR` | Yes | Node role: `Committee`, `Operator`, `Challenger`, `Watchtower` | `Challenger` | +| `ACTOR` | Yes | Node role: `Committee`, `Operator`, `Verifier`, `Watchtower` | `Verifier` | | `BITCOIN_NETWORK` | Yes | Bitcoin network: `bitcoin`, `testnet4`, `signet`, `regtest` | `testnet4` | | `GOAT_NETWORK` | Yes | GOAT network: `main`, `test` | `test` | | `GOAT_CHAIN_URL` | Yes | GOAT L2 RPC endpoint | - | @@ -848,7 +870,7 @@ bitvm2-noded \ | `BITVM_SECRET` | Yes | Node private key or seed (`seed:xxx` format) | - | | `PEER_KEY` | Yes | libp2p node key (Base64 encoded) | - | | `GOAT_PRIVATE_KEY` | Conditional | GOAT chain private key (required for Committee) | - | -| `GOAT_ADDRESS` | Conditional | GOAT address (required for Operator/Challenger) | - | +| `GOAT_ADDRESS` | Conditional | GOAT address (required for Operator/Verifier) | - | | `ENABLE_RELAYER` | No | Enable relayer mode for Committee nodes | `false` | | `BTC_CHAIN_URL` | No | Bitcoin Esplora API endpoint | Public Esplora | | `MARA_SLIPSTREAM_API_URL` | No | MARA slipstream API base URL (used for non-standard tx broadcast) | mainnet: `https://slipstream.mara.com/api`; testnet4: `https://teststream.mara.com/api` | @@ -888,11 +910,11 @@ Relayer nodes should: | `/instances` | GET | List all instances | | `/instance/:id` | GET | Get instance details | | `/instances/overview` | GET | Instance statistics overview | -| `/graphs` | GET | List all graphs | -| `/graph/:id` | GET | Get graph details | -| `/graph/:id/txn` | GET | Get graph transaction list | -| `/graph/:id/tx/:txid` | GET | Get specific transaction | -| `/graph/ready_to_kickoff` | GET | Get graphs ready for kickoff | +| `/v1/graphs` | GET | List all graphs | +| `/v1/graphs/:id` | GET | Get graph details | +| `/v1/graphs/:id/txn?cursor=0` | GET | Get graph transaction list | +| `/v1/graphs/:id/tx?tx_name=cur-pre-kickoff.hex` | GET | Get specific transaction hex | +| `/v1/graphs/ready-to-kickoff` | GET | Get graphs ready for kickoff | | `/bridge_in_request` | POST | Initiate Bridge-In request | | `/bridge_out_init` | POST | Initiate Bridge-Out request | | `/challenge` | POST | Submit challenge | @@ -905,4 +927,4 @@ Relayer nodes should: - [Architecture Document](../docs/ARCHITECTURE.md) - Full system architecture - [Circuits README](../circuits/README.md) - ZK proof generation - [Deployment Guide](../deployment/README.md) - Deployment instructions -- [Incentives](../docs/incentives.md) - Incentives \ No newline at end of file +- [Incentives](../docs/incentives.md) - Incentives diff --git a/node/entrypoint.sh b/node/entrypoint.sh index 9bb31fb1..4e4d6b66 100755 --- a/node/entrypoint.sh +++ b/node/entrypoint.sh @@ -5,4 +5,4 @@ if [ -n "$BOOTNODES" ]; then bn="--bootnodes $BOOTNODES" fi -bitvm2-noded --rpc-addr 0.0.0.0:9100 --db-path /var/data/bitvm2-node-0.db --p2p-port 8443 $bn +bitvm-noded --rpc-addr 0.0.0.0:9100 --db-path /var/data/bitvm-node-0.db --p2p-port 8443 $bn diff --git a/node/src/action.rs b/node/src/action.rs index 47219f23..08e22cbf 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -8,11 +8,15 @@ use crate::middleware::AllBehaviours; use crate::rpc_service::current_time_secs; use crate::utils::*; use alloy::primitives::Address as EvmAddress; -use anyhow::{Result, anyhow}; +use anyhow::{Context, Result, anyhow}; use bitcoin::{PublicKey, Txid}; -use bitvm2_lib::actors::Actor; -use bitvm2_lib::committee::*; -use bitvm2_lib::types::{Bitvm2Graph, SimplifiedBitvm2Graph}; +use bitvm_lib::actors::Actor; +use bitvm_lib::babe_adapter::{ + BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, + CACSetupPackage, +}; +use bitvm_lib::committee::*; +use bitvm_lib::types::{BitvmGcGraph, SimplifiedBitvmGcGraph}; use client::goat_chain::DisproveTxType; use client::http_client::async_client::HttpAsyncClient; use client::{btc_chain::BTCClient, goat_chain::GOATClient}; @@ -21,6 +25,7 @@ use libp2p::{PeerId, Swarm, gossipsub}; use musig2::{PartialSignature, PubNonce}; use secp256k1::schnorr::Signature as SchnorrSignature; use serde::{Deserialize, Serialize}; +use std::sync::Arc; use store::MessageState; use store::localdb::LocalDB; use tracing::warn; @@ -32,11 +37,17 @@ pub struct GOATMessage { pub content: GOATMessageContent, } +const GOAT_MESSAGE_BIN_PREFIX: &[u8] = b"GOATBIN1"; + #[derive(Serialize, Deserialize, Clone)] pub enum GOATMessageContent { PeginRequest(PeginRequest), CreateGraph(CreateGraph), ConfirmInstance(ConfirmInstance), + InitGraph(InitGraph), + GenCircuits(GenCircuits), + CutCircuits(CutCircuits), + SolderingProofReady(SolderingProofReady), NonceGeneration(NonceGeneration), CommitteePresign(CommitteePresign), EndorseGraph(EndorseGraph), @@ -51,12 +62,13 @@ pub enum GOATMessageContent { WatchtowerChallengeInitSent(WatchtowerChallengeInitSent), WatchtowerChallengeSent(WatchtowerChallengeSent), WatchtowerChallengeTimeout(WatchtowerChallengeTimeout), - OperatorAckTimeout(OperatorAckTimeout), - OperatorCommitBlockHashReady(OperatorCommitBlockHashReady), - OperatorCommitBlockHashTimeout(OperatorCommitBlockHashTimeout), - AssertInitReady(AssertInitReady), - AssertCommitTimeout(AssertCommitTimeout), - DisproveReady(DisproveReady), + NackReady(NackReady), + OperatorCommitPubinReady(OperatorCommitPubinReady), + OperatorCommitPubinTimeout(OperatorCommitPubinTimeout), + AssertReady(AssertReady), + AssertSent(AssertSent), + ChallengeAssertSent(ChallengeAssertSent), + WronglyChallengeTimeout(WronglyChallengeTimeout), DisproveSent(DisproveSent), Take1Ready(Take1Ready), Take1Sent(Take1Sent), @@ -84,19 +96,45 @@ pub struct ConfirmInstance { pub instance_id: Uuid, } #[derive(Serialize, Deserialize, Clone)] +pub struct InitGraph { + pub instance_id: Uuid, + pub graph_id: Uuid, +} +#[derive(Serialize, Deserialize, Clone)] +pub struct GenCircuits { + pub instance_id: Uuid, + pub graph_id: Uuid, + pub verifier_pubkey: PublicKey, + pub setup_package: CACSetupPackage, +} +#[derive(Serialize, Deserialize, Clone)] +pub struct CutCircuits { + pub instance_id: Uuid, + pub graph_id: Uuid, + pub verifier_pubkey: PublicKey, + pub verifier_index: usize, + pub selected_circuit_indexes: Vec, +} +#[derive(Serialize, Deserialize, Clone)] +pub struct SolderingProofReady { + pub instance_id: Uuid, + pub graph_id: Uuid, + pub verifier_index: usize, + pub payload_hash: [u8; 32], + pub total_len: usize, +} +#[derive(Serialize, Deserialize, Clone)] pub struct CreateGraph { pub instance_id: Uuid, pub graph_id: Uuid, pub graph_nonce: u64, - pub graph: SimplifiedBitvm2Graph, + pub graph: SimplifiedBitvmGcGraph, } #[derive(Serialize, Deserialize, Clone)] pub struct NonceGeneration { pub instance_id: Uuid, pub graph_id: Uuid, pub committee_pubkey: PublicKey, - pub watchtower_num: usize, - pub assert_commit_num: usize, pub pub_nonces: CommitteePubNonces, pub nonce_sigs: CommitteeNonceSignatures, } @@ -121,7 +159,7 @@ pub struct GraphFinalize { pub instance_id: Uuid, pub graph_id: Uuid, pub graph_nonce: u64, - pub graph: SimplifiedBitvm2Graph, + pub graph: SimplifiedBitvmGcGraph, pub endorse_sigs: Vec<(PublicKey, EvmAddress, Vec)>, } #[derive(Serialize, Deserialize, Clone)] @@ -175,43 +213,55 @@ pub struct WatchtowerChallengeInitSent { pub struct WatchtowerChallengeSent { pub instance_id: Uuid, pub graph_id: Uuid, - pub watchtower_challenge_txids: Vec<(usize, Txid)>, + pub watchtower_index: usize, } #[derive(Serialize, Deserialize, Clone)] pub struct WatchtowerChallengeTimeout { pub instance_id: Uuid, pub graph_id: Uuid, - pub watchtower_indexes: Vec, } #[derive(Serialize, Deserialize, Clone)] -pub struct OperatorAckTimeout { +pub struct NackReady { + pub instance_id: Uuid, + pub graph_id: Uuid, +} +#[derive(Serialize, Deserialize, Clone)] +pub struct OperatorCommitPubinReady { pub instance_id: Uuid, pub graph_id: Uuid, } #[derive(Serialize, Deserialize, Clone)] -pub struct OperatorCommitBlockHashReady { +pub struct OperatorCommitPubinTimeout { pub instance_id: Uuid, pub graph_id: Uuid, } #[derive(Serialize, Deserialize, Clone)] -pub struct OperatorCommitBlockHashTimeout { +pub struct AssertReady { pub instance_id: Uuid, pub graph_id: Uuid, } #[derive(Serialize, Deserialize, Clone)] -pub struct AssertInitReady { +pub struct AssertSent { pub instance_id: Uuid, pub graph_id: Uuid, + pub assert_txid: Txid, + pub assert_witness: Option, } #[derive(Serialize, Deserialize, Clone)] -pub struct AssertCommitTimeout { +pub struct ChallengeAssertSent { pub instance_id: Uuid, pub graph_id: Uuid, + pub challenge_assert_txid: Txid, + pub verifier_index: usize, + pub challenge_witness: Option, } #[derive(Serialize, Deserialize, Clone)] -pub struct DisproveReady { +pub struct WronglyChallengeTimeout { pub instance_id: Uuid, pub graph_id: Uuid, + pub challenge_assert_txid: Txid, + pub verifier_index: usize, + pub wrongly_challenged_witness: Option, } #[derive(Serialize, Deserialize, Clone)] pub struct DisproveSent { @@ -267,7 +317,7 @@ pub struct SyncGraphRequest { pub struct SyncGraph { pub instance_id: Uuid, pub graph_id: Uuid, - pub graph: SimplifiedBitvm2Graph, + pub graph: SimplifiedBitvmGcGraph, } #[derive(Serialize, Deserialize, Clone)] @@ -291,12 +341,32 @@ impl GOATMessage { pub async fn serialize_message(&self) -> Result> { let cloned = self.clone(); - Ok(tokio::task::spawn_blocking(move || serde_json::to_vec(&cloned)).await??) + tokio::task::spawn_blocking(move || { + if matches!(&cloned.content, GOATMessageContent::GenCircuits(_)) { + let mut encoded = bincode::serialize(&cloned) + .context("failed to serialize bincode GOATMessage")?; + let mut message = Vec::with_capacity(GOAT_MESSAGE_BIN_PREFIX.len() + encoded.len()); + message.extend_from_slice(GOAT_MESSAGE_BIN_PREFIX); + message.append(&mut encoded); + Ok(message) + } else { + serde_json::to_vec(&cloned).context("failed to serialize legacy JSON GOATMessage") + } + }) + .await? } pub async fn deserialize_message(message: &[u8]) -> Result { let cloned = message.to_vec(); - Ok(tokio::task::spawn_blocking(move || serde_json::from_slice(&cloned)).await??) + tokio::task::spawn_blocking(move || { + if let Some(encoded) = cloned.strip_prefix(GOAT_MESSAGE_BIN_PREFIX) { + bincode::deserialize(encoded).context("failed to deserialize bincode GOATMessage") + } else { + serde_json::from_slice(&cloned) + .context("failed to deserialize legacy JSON GOATMessage") + } + }) + .await? } } #[allow(clippy::too_many_arguments)] @@ -306,6 +376,7 @@ pub async fn handle_self_p2p_msg( btc_client: &BTCClient, goat_client: &GOATClient, http_client: &HttpAsyncClient, + soldering_builder: &Option>, actor: Actor, from_peer_id: PeerId, id: MessageId, @@ -332,6 +403,7 @@ pub async fn handle_self_p2p_msg( btc_client, goat_client, http_client, + soldering_builder, actor.clone(), from_peer_id, id.clone(), @@ -380,6 +452,7 @@ pub async fn recv_and_dispatch( btc_client: &BTCClient, goat_client: &GOATClient, http_client: &HttpAsyncClient, + soldering_builder: &Option>, actor: Actor, from_peer_id: PeerId, id: MessageId, @@ -397,6 +470,7 @@ pub async fn recv_and_dispatch( btc_client, goat_client, http_client, + soldering_builder, actor, from_peer_id, id, @@ -411,7 +485,7 @@ pub async fn try_finalize_graph( goat_client: &GOATClient, instance_id: Uuid, graph_id: Uuid, - graph: Option<&SimplifiedBitvm2Graph>, + graph: Option<&SimplifiedBitvmGcGraph>, broadcast_graph_finalize: bool, ) -> Result<()> { let endorsements = @@ -425,12 +499,12 @@ pub async fn try_finalize_graph( && partial_sigs.len() == committee_pubkeys.len() { let mut graph = match graph { - Some(g) => Bitvm2Graph::from_simplified(g)?, + Some(g) => BitvmGcGraph::from_simplified(g)?, None => { let g = get_graph(local_db, instance_id, graph_id) .await? .ok_or_else(|| anyhow!("Graph not found for {instance_id}:{graph_id}"))?; - Bitvm2Graph::from_simplified(&g)? + BitvmGcGraph::from_simplified(&g)? } }; let pub_nonces = pub_nonoces.into_iter().map(|(_, pn)| pn).collect::>(); @@ -498,7 +572,7 @@ pub(crate) async fn get_graph_or_defer( instance_id: Uuid, graph_id: Uuid, message: &GOATMessage, -) -> Result> { +) -> Result> { match get_graph(local_db, instance_id, graph_id).await? { Some(g) => Ok(Some(g)), None => { @@ -537,3 +611,32 @@ pub async fn try_send_sync_graph_request( send_to_peer(swarm, message).await?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn soldering_proof_ready_is_descriptor_only() { + let ready = SolderingProofReady { + instance_id: Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(), + graph_id: Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap(), + verifier_index: 3, + payload_hash: [0xabu8; 32], + total_len: 1024, + }; + + let value = serde_json::to_value(ready).unwrap(); + let object = value.as_object().unwrap(); + + assert!(object.contains_key("instance_id")); + assert!(object.contains_key("graph_id")); + assert!(object.contains_key("verifier_index")); + assert!(object.contains_key("payload_hash")); + assert!(object.contains_key("total_len")); + assert!(!object.contains_key("payload_path")); + assert!(!object.contains_key("payload")); + assert!(!object.contains_key("setup_package")); + assert!(!object.contains_key("verifier_pubkey")); + } +} diff --git a/node/src/bin/db_inject.rs b/node/src/bin/db_inject.rs index 66fa272b..4a7e9cc2 100644 --- a/node/src/bin/db_inject.rs +++ b/node/src/bin/db_inject.rs @@ -5,14 +5,14 @@ //! can process it as if it were received from the network. //! //! Key args: -//! - --db-path: local SQLite path (e.g., sqlite:/tmp/bitvm2-node.db) -//! - --actor: Committee | Operator | Challenger | Watchtower | All +//! - --db-path: local SQLite path (e.g., sqlite:/tmp/bitvm-node.db) +//! - --actor: Committee | Operator | Verifier | Watchtower | All //! - --message-json or --message-file (one required) //! - --business-id (optional; inferred from content when possible) //! //! Example: -//! - cargo run -p bitvm2-noded --bin update-db -- \ -//! --db-path sqlite:/tmp/bitvm2-node.db \ +//! - cargo run -p bitvm-noded --bin update-db -- \ +//! --db-path sqlite:/tmp/bitvm-node.db \ //! --actor Operator \ //! --message-file ./message.json use std::fs; @@ -23,9 +23,9 @@ use anyhow::{Context, Result, anyhow}; use clap::Parser; use uuid::Uuid; -use bitvm2_lib::actors::Actor; -use bitvm2_noded::action::*; -use bitvm2_noded::utils::upsert_message; +use bitvm_lib::actors::Actor; +use bitvm_noded::action::*; +use bitvm_noded::utils::upsert_message; use store::create_local_db; #[derive(Parser, Debug)] @@ -35,7 +35,7 @@ struct Args { #[arg(long)] db_path: String, - /// Target actor (Committee, Operator, Challenger, Watchtower, All) + /// Target actor (Committee, Operator, Verifier, Watchtower, All) #[arg(long, value_parser = parse_actor)] actor: Actor, @@ -80,6 +80,10 @@ fn infer_business_id(content: &GOATMessageContent) -> Option { match content { GOATMessageContent::PeginRequest(v) => Some(v.instance_id), GOATMessageContent::ConfirmInstance(v) => Some(v.instance_id), + GOATMessageContent::InitGraph(v) => Some(v.instance_id), + GOATMessageContent::GenCircuits(v) => Some(v.instance_id), + GOATMessageContent::CutCircuits(v) => Some(v.instance_id), + GOATMessageContent::SolderingProofReady(v) => Some(v.instance_id), GOATMessageContent::CreateGraph(v) => Some(v.graph_id), GOATMessageContent::NonceGeneration(v) => Some(v.graph_id), GOATMessageContent::CommitteePresign(v) => Some(v.graph_id), @@ -95,12 +99,13 @@ fn infer_business_id(content: &GOATMessageContent) -> Option { GOATMessageContent::WatchtowerChallengeInitSent(v) => Some(v.graph_id), GOATMessageContent::WatchtowerChallengeSent(v) => Some(v.graph_id), GOATMessageContent::WatchtowerChallengeTimeout(v) => Some(v.graph_id), - GOATMessageContent::OperatorAckTimeout(v) => Some(v.graph_id), - GOATMessageContent::OperatorCommitBlockHashReady(v) => Some(v.graph_id), - GOATMessageContent::OperatorCommitBlockHashTimeout(v) => Some(v.graph_id), - GOATMessageContent::AssertInitReady(v) => Some(v.graph_id), - GOATMessageContent::AssertCommitTimeout(v) => Some(v.graph_id), - GOATMessageContent::DisproveReady(v) => Some(v.graph_id), + GOATMessageContent::NackReady(v) => Some(v.graph_id), + GOATMessageContent::OperatorCommitPubinReady(v) => Some(v.graph_id), + GOATMessageContent::OperatorCommitPubinTimeout(v) => Some(v.graph_id), + GOATMessageContent::AssertReady(v) => Some(v.graph_id), + GOATMessageContent::AssertSent(v) => Some(v.graph_id), + GOATMessageContent::ChallengeAssertSent(v) => Some(v.graph_id), + GOATMessageContent::WronglyChallengeTimeout(v) => Some(v.graph_id), GOATMessageContent::DisproveSent(v) => Some(v.graph_id), GOATMessageContent::Take1Ready(v) => Some(v.graph_id), GOATMessageContent::Take1Sent(v) => Some(v.graph_id), diff --git a/node/src/bin/fetch_watchtower_xonly_pubkeys.rs b/node/src/bin/fetch_watchtower_xonly_pubkeys.rs new file mode 100644 index 00000000..3a67094a --- /dev/null +++ b/node/src/bin/fetch_watchtower_xonly_pubkeys.rs @@ -0,0 +1,108 @@ +use alloy::primitives::Address as EvmAddress; +use anyhow::{Context, Result, anyhow}; +use bitvm_noded::env::{ + ENV_GOAT_CHAIN_URL, ENV_GOAT_GATEWAY_CONTRACT_ADDRESS, ENV_GOAT_NETWORK, + get_goat_gateway_contract_from_env, get_goat_network, goat_config_from_env, +}; +use client::goat_chain::GOATClient; +use serde::Serialize; +use sha2::{Digest, Sha256}; +use std::{ + env, fs, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; + +#[derive(Debug, Serialize)] +struct WatchtowerSnapshot { + goat_chain_url: String, + goat_gateway_contract_address: String, + goat_network: String, + committee_management_address: String, + watchtower_xonly_public_keys: Vec, + watchtower_count: usize, + watchtower_list_hash: String, + watchtower_order_note: String, + generated_at_unix: u64, +} + +fn require_env(name: &str) -> Result { + env::var(name).map_err(|_| anyhow!("{name} must be set")) +} + +fn workspace_root() -> PathBuf { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("node crate is under the workspace root") + .to_path_buf() +} + +fn snapshot_path(gateway: &EvmAddress) -> PathBuf { + workspace_root() + .join("target") + .join("watchtower-snapshots") + .join(format!("{}.json", gateway.to_string().to_lowercase())) +} + +fn watchtower_list_hash(watchtower_pubkeys: &[String]) -> String { + let mut hasher = Sha256::new(); + // The hash is intentionally order-sensitive because watchtower index maps to node_index. + for key in watchtower_pubkeys { + hasher.update(key.as_bytes()); + } + format!("0x{}", hex::encode(hasher.finalize())) +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + + let goat_chain_url = require_env(ENV_GOAT_CHAIN_URL)?; + let goat_gateway_contract_address = require_env(ENV_GOAT_GATEWAY_CONTRACT_ADDRESS)?; + let goat_network = env::var(ENV_GOAT_NETWORK).unwrap_or_else(|_| "test".to_string()); + + let gateway = get_goat_gateway_contract_from_env(); + let goat_client = GOATClient::new(goat_config_from_env().await, get_goat_network()); + let committee_management_address = + EvmAddress::from_slice(&goat_client.gateway_get_committee_management().await?); + // Preserve on-chain order. The operator circuit compares this list by index with graph + // watchtower_pubkeys, watchtower_challenge vouts, and included_watchtowers bitmap bits. + let watchtower_pubkeys = goat_client + .committee_mana_get_watchtowers() + .await? + .into_iter() + .map(|key| format!("0x{}", hex::encode(key.serialize()))) + .collect::>(); + + if watchtower_pubkeys.is_empty() { + return Err(anyhow!("committee management returned an empty watchtower list")); + } + + let watchtower_list_hash = watchtower_list_hash(&watchtower_pubkeys); + let snapshot = WatchtowerSnapshot { + goat_chain_url, + goat_gateway_contract_address, + goat_network, + committee_management_address: committee_management_address.to_string(), + watchtower_count: watchtower_pubkeys.len(), + watchtower_xonly_public_keys: watchtower_pubkeys.clone(), + watchtower_list_hash: watchtower_list_hash.clone(), + watchtower_order_note: + "order-sensitive: index must match graph watchtower_pubkeys/node_index".to_string(), + generated_at_unix: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), + }; + + let path = snapshot_path(&gateway); + let parent = path.parent().context("snapshot path has parent")?; + fs::create_dir_all(parent)?; + fs::write(&path, serde_json::to_string_pretty(&snapshot)?)?; + + eprintln!("watchtower snapshot: {}", path.display()); + eprintln!("watchtower list hash: {watchtower_list_hash}"); + eprintln!( + "before build, export FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS or eval this command output" + ); + println!("export FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS={}", watchtower_pubkeys.join(",")); + + Ok(()) +} diff --git a/node/src/bin/mock_rpc.rs b/node/src/bin/mock_rpc.rs new file mode 100644 index 00000000..c9e08644 --- /dev/null +++ b/node/src/bin/mock_rpc.rs @@ -0,0 +1,373 @@ +//! Local-only mock RPC service for interface testing. +//! +//! This starts the Axum RPC routes without P2P, chain watchers, or maintenance tasks, +//! and seeds SQLite with deterministic demo rows. + +use std::str::FromStr; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; + +use alloy::primitives::U256; +use anyhow::{Context, Result}; +use bitcoin::Network; +use bitvm_lib::actors::Actor; +use bitvm_noded::env::{ + ENV_BITCOIN_NETWORK, ENV_BITVM_SECRET, ENV_GOAT_ADDRESS, ENV_GOAT_GATEWAY_CONTRACT_ADDRESS, + ENV_GOAT_NETWORK, ENV_GOAT_SWAP_CONTRACT_ADDRESS, +}; +use bitvm_noded::rpc_service::{self, AppState, current_time_secs}; +use bitvm_noded::utils::{generate_local_key, get_rand_btc_address_p2wpkh, get_rand_goat_address}; +use clap::Parser; +use client::Utxo; +use prometheus_client::registry::Registry; +use secp256k1::Secp256k1; +use store::{ + BridgeOutGlobalStats, GoatTxProcessingStatus, GoatTxRecord, GoatTxType, Graph, GraphStatus, + Instance, InstanceBridgeInStatus, InstanceBridgeOutStatus, Node, UInt64Array3, create_local_db, +}; +use tokio::signal; +use tokio_util::sync::CancellationToken; +use tracing_subscriber::EnvFilter; +use uuid::Uuid; + +const MOCK_GATEWAY_CONTRACT: &str = "0x1111111111111111111111111111111111111111"; +const MOCK_SWAP_CONTRACT: &str = "0x2222222222222222222222222222222222222222"; + +#[derive(Debug, Parser)] +#[command(author, version, about = "Start a local mock BitVM RPC service")] +struct Opts { + /// Local RPC service address. + #[arg(long, default_value = "127.0.0.1:18080")] + rpc_addr: String, + + /// SQLite database URL or file path. Defaults to a timestamped /tmp database. + #[arg(long)] + db_path: Option, + + /// Current node role exposed through AppState. + #[arg(long, default_value = "Verifier", value_parser = parse_actor)] + actor: Actor, +} + +#[derive(Debug)] +struct MockSeedSummary { + bridge_in_instance_id: Uuid, + bridge_out_instance_id: Uuid, + ready_graph_id: Uuid, + challenge_graph_id: Uuid, + operator_pubkey: String, + graph_from_addr: String, +} + +fn parse_actor(raw: &str) -> std::result::Result { + Actor::from_str(raw).map_err(|_| format!("invalid actor: {raw}")) +} + +fn default_db_path() -> String { + let ts = SystemTime::now().duration_since(UNIX_EPOCH).map(|v| v.as_secs()).unwrap_or(0); + format!("sqlite:/tmp/bitvm-node-mock-{ts}.db") +} + +fn normalize_db_path(path: Option) -> String { + let path = path.unwrap_or_else(default_db_path); + if path.starts_with("sqlite:") { path } else { format!("sqlite:{path}") } +} + +fn set_env_default(key: &str, value: &str) { + if std::env::var_os(key).is_none() { + unsafe { + std::env::set_var(key, value); + } + } +} + +fn configure_mock_env() { + set_env_default("RUST_LOG", "info"); + set_env_default(ENV_BITCOIN_NETWORK, "testnet4"); + set_env_default(ENV_GOAT_NETWORK, "test"); + set_env_default(ENV_GOAT_GATEWAY_CONTRACT_ADDRESS, MOCK_GATEWAY_CONTRACT); + set_env_default(ENV_GOAT_SWAP_CONTRACT_ADDRESS, MOCK_SWAP_CONTRACT); + set_env_default(ENV_BITVM_SECRET, "seed:mock-rpc-service"); + set_env_default(ENV_GOAT_ADDRESS, "0x3333333333333333333333333333333333333333"); +} + +fn mock_uuid(raw: &str) -> Uuid { + Uuid::parse_str(raw).expect("valid mock UUID") +} + +fn mock_tx_hash(byte: u8) -> String { + format!("0x{}", hex::encode([byte; 32])) +} + +fn mock_goat_addr(byte: u8) -> String { + format!("0x{}", hex::encode([byte; 20])) +} + +fn mock_btc_pubkey() -> String { + let (_, public_key) = Secp256k1::new().generate_keypair(&mut rand::thread_rng()); + public_key.to_string() +} + +fn mock_peer_id() -> String { + generate_local_key().public().to_peer_id().to_string() +} + +fn seeded_node( + peer_id: String, + actor: Actor, + node_name: &str, + btc_pub_key: String, + now: i64, +) -> Node { + Node { + peer_id, + actor: actor.to_string(), + node_name: node_name.to_string(), + goat_addr: get_rand_goat_address(), + btc_pub_key, + socket_addr: "127.0.0.1:18080".to_string(), + reward: "0".to_string(), + service_fee_rate: 0.001, + available_peg_btc: U256::from(4_700_000_000_000_000_000_000_000_u128).to_string(), + updated_at: now, + created_at: now, + } +} + +fn seeded_bridge_in_instance( + instance_id: Uuid, + status: InstanceBridgeInStatus, + amount: i64, + now: i64, + utxo_byte: u8, +) -> Result { + let utxo = vec![Utxo { txid: [utxo_byte; 32], vout: 0, amount_sats: amount as u64 }]; + Ok(Instance { + instance_id, + is_bridge_in: true, + network: Network::Testnet4.to_string(), + from_addr: get_rand_btc_address_p2wpkh(Network::Testnet4), + to_addr: mock_goat_addr(0x31), + amount, + fees: UInt64Array3([1_000, 2_000, 3_000]), + input_utxos: serde_json::to_string(&utxo)?, + status: status.to_string(), + goat_tx_hash: mock_tx_hash(0xa1), + goat_tx_height: 123_456, + user_change_addr: get_rand_btc_address_p2wpkh(Network::Testnet4), + user_refund_addr: get_rand_btc_address_p2wpkh(Network::Testnet4), + pegin_data_tx_hash: mock_tx_hash(0xa2), + post_pegin_txhash: Some(mock_tx_hash(0xa3)), + bridge_out_amount: "0".to_string(), + status_updated_at: now - 120, + created_at: now - 3_600, + updated_at: now, + ..Default::default() + }) +} + +fn seeded_bridge_out_instance(instance_id: Uuid, now: i64, escrow_hash: String) -> Instance { + Instance { + instance_id, + is_bridge_in: false, + network: Network::Testnet4.to_string(), + from_addr: mock_goat_addr(0x41), + to_addr: get_rand_btc_address_p2wpkh(Network::Testnet4), + amount: 0, + input_utxos: "[]".to_string(), + status: InstanceBridgeOutStatus::Initialize.to_string(), + goat_tx_hash: mock_tx_hash(0xb1), + goat_tx_height: 123_500, + escrow_hash: Some(escrow_hash), + bridge_out_amount: "25000000".to_string(), + bridge_out_lock_time: now + 3_600, + status_updated_at: now - 60, + created_at: now - 900, + updated_at: now, + ..Default::default() + } +} + +#[allow(clippy::too_many_arguments)] +fn seeded_graph( + graph_id: Uuid, + instance_id: Uuid, + kickoff_index: i64, + status: GraphStatus, + operator_pubkey: &str, + from_addr: &str, + now: i64, + init_withdraw_tx_hash: Option, + sub_status: String, +) -> Graph { + Graph { + graph_id, + instance_id, + kickoff_index, + from_addr: from_addr.to_string(), + to_addr: get_rand_btc_address_p2wpkh(Network::Testnet4), + amount: 10_000_000, + challenge_amount: 1_000_000, + status: status.to_string(), + sub_status, + operator_pubkey: operator_pubkey.to_string(), + init_withdraw_tx_hash, + bridge_out_start_at: now - 300, + status_updated_at: now - 120, + proceed_withdraw_height: 899_980, + created_at: now - 7_200, + updated_at: now, + ..Default::default() + } +} + +async fn seed_mock_data( + local_db: &store::localdb::LocalDB, + current_peer_id: &str, + actor: Actor, +) -> Result { + let now = current_time_secs(); + let operator_pubkey = mock_btc_pubkey(); + let graph_from_addr = mock_goat_addr(0x51); + let bridge_in_instance_id = mock_uuid("11111111-1111-1111-1111-111111111111"); + let bridge_in_pending_id = mock_uuid("11111111-1111-1111-1111-111111111112"); + let bridge_out_instance_id = mock_uuid("22222222-2222-2222-2222-222222222222"); + let ready_graph_id = mock_uuid("33333333-3333-3333-3333-333333333333"); + let challenge_graph_id = mock_uuid("33333333-3333-3333-3333-333333333334"); + let escrow_hash = mock_tx_hash(0xc1); + + let current_node = + seeded_node(current_peer_id.to_string(), actor, "mock-current", mock_btc_pubkey(), now); + let committee_node = + seeded_node(mock_peer_id(), Actor::Committee, "mock-committee", mock_btc_pubkey(), now); + let operator_node = + seeded_node(mock_peer_id(), Actor::Operator, "mock-operator", operator_pubkey.clone(), now); + let watchtower_node = + seeded_node(mock_peer_id(), Actor::Watchtower, "mock-watchtower", mock_btc_pubkey(), now); + + let bridge_in_success = seeded_bridge_in_instance( + bridge_in_instance_id, + InstanceBridgeInStatus::RelayerL2Minted, + 10_000_000, + now, + 0x11, + )?; + let bridge_in_pending = seeded_bridge_in_instance( + bridge_in_pending_id, + InstanceBridgeInStatus::UserInited, + 1_000_000, + now, + 0x12, + )?; + let bridge_out = seeded_bridge_out_instance(bridge_out_instance_id, now, escrow_hash.clone()); + + let none_sub_status = r#"{"watchtower_challenge_status":[],"verifier_challenge_status":[],"disprove_type":null,"disprove_index":-1}"#.to_string(); + let challenge_sub_status = r#"{"watchtower_challenge_status":[true,false],"verifier_challenge_status":["None"],"disprove_type":null,"disprove_index":-1}"#.to_string(); + let ready_graph = seeded_graph( + ready_graph_id, + bridge_in_instance_id, + 0, + GraphStatus::OperatorDataPushed, + &operator_pubkey, + &graph_from_addr, + now, + None, + none_sub_status.clone(), + ); + let challenge_graph = seeded_graph( + challenge_graph_id, + bridge_in_instance_id, + 1, + GraphStatus::Challenge, + &operator_pubkey, + &graph_from_addr, + now, + Some(mock_tx_hash(0xd1)), + challenge_sub_status, + ); + + let mut tx = local_db.start_transaction().await?; + for node in [current_node, committee_node, operator_node, watchtower_node] { + tx.upsert_node(&node).await?; + } + for instance in [bridge_in_success, bridge_in_pending, bridge_out] { + tx.upsert_instance(&instance).await?; + } + for graph in [ready_graph, challenge_graph] { + tx.upsert_graph(&graph).await?; + } + tx.upsert_goat_tx_record(&GoatTxRecord { + instance_id: bridge_out_instance_id, + graph_id: Uuid::nil(), + tx_type: GoatTxType::SwapInitialize.to_string(), + tx_hash: mock_tx_hash(0xe1), + height: 123_501, + is_local: true, + processing_status: GoatTxProcessingStatus::Processed.to_string(), + extra: Some(escrow_hash), + created_at: now, + }) + .await?; + tx.upsert_bridge_out_global_stats(&BridgeOutGlobalStats { + id: 1, + initial_txn: 1, + initial_amount: "25000000".to_string(), + claim_txn: 1, + claim_amount: "12000000".to_string(), + refund_txn: 0, + refund_amount: "0".to_string(), + created_at: now, + updated_at: now, + }) + .await?; + tx.commit().await?; + + Ok(MockSeedSummary { + bridge_in_instance_id, + bridge_out_instance_id, + ready_graph_id, + challenge_graph_id, + operator_pubkey, + graph_from_addr, + }) +} + +#[tokio::main(flavor = "current_thread")] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + configure_mock_env(); + let _ = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).try_init(); + + let opts = Opts::parse(); + let db_path = normalize_db_path(opts.db_path); + let local_key = generate_local_key(); + let peer_id = local_key.public().to_peer_id().to_string(); + let local_db = create_local_db(&db_path).await; + let seed_summary = + seed_mock_data(&local_db, &peer_id, opts.actor.clone()).await.context("seed mock data")?; + + let registry = Arc::new(Mutex::new(Registry::default())); + let app_state = + AppState::create_arc_mock_app_state(local_db, opts.actor, peer_id, registry).await?; + let cancellation_token = CancellationToken::new(); + let shutdown_token = cancellation_token.clone(); + tokio::spawn(async move { + if signal::ctrl_c().await.is_ok() { + shutdown_token.cancel(); + } + }); + + println!("Mock RPC listening on http://{}", opts.rpc_addr); + println!("DB path: {db_path}"); + println!("Gateway contract: {MOCK_GATEWAY_CONTRACT}"); + println!("Swap contract: {MOCK_SWAP_CONTRACT}"); + println!("Bridge-in instance: {}", seed_summary.bridge_in_instance_id); + println!("Bridge-out instance: {}", seed_summary.bridge_out_instance_id); + println!("Ready graph: {}", seed_summary.ready_graph_id); + println!("Challenge graph: {}", seed_summary.challenge_graph_id); + println!("Operator pubkey: {}", seed_summary.operator_pubkey); + println!("Graph from_addr: {}", seed_summary.graph_from_addr); + + rpc_service::serve_with_app_state(opts.rpc_addr, app_state, cancellation_token).await?; + Ok(()) +} diff --git a/node/src/bin/part_stark_vk_attest.rs b/node/src/bin/part_stark_vk_attest.rs deleted file mode 100644 index 411fec7e..00000000 --- a/node/src/bin/part_stark_vk_attest.rs +++ /dev/null @@ -1,246 +0,0 @@ -use anyhow::{Context, Result, bail}; -use bitcoin::PrivateKey; -use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; -use bitcoin_light_client_circuit::{ - part_stark_vk_attestation_dir, save_latest_part_stark_vk_attestation_snapshot, - sign_latest_part_stark_vk_snapshot, -}; -use clap::{Parser, Subcommand}; -use commit_chain::CommitInfo; -use semver::Version; -use std::path::PathBuf; -use std::str::FromStr; -use zkm_verifier::Groth16Verifier; - -#[derive(Debug, Parser)] -struct Cli { - #[command(subcommand)] - command: Command, -} - -#[derive(Debug, Subcommand)] -enum Command { - BuildTree { - #[arg(long, value_delimiter = ',')] - versions: Vec, - #[arg(long)] - attestation_dir: Option, - }, - SignRoot { - #[arg(long, value_delimiter = ',')] - versions: Vec, - #[arg(long)] - commit_info_file: PathBuf, - #[arg(long, value_parser = decode_publisher_secret_key_wif)] - publisher_secret_key_wif: SecretKey, - #[arg(long)] - attestation_dir: Option, - }, -} - -fn resolve_attestation_dir(attestation_dir: Option) -> PathBuf { - attestation_dir.unwrap_or_else(part_stark_vk_attestation_dir) -} - -fn normalize_version(version: &str) -> Result { - let raw_version = version.trim(); - if raw_version.is_empty() { - bail!("version cannot be empty"); - } - let normalized = raw_version - .strip_prefix('v') - .or_else(|| raw_version.strip_prefix('V')) - .unwrap_or(raw_version); - Version::parse(normalized).with_context(|| format!("invalid semver version '{version}'")) -} - -/// Sort versions by semantic version while rejecting duplicate normalized versions. -fn order_versions(versions: &[String]) -> Result> { - if versions.is_empty() { - bail!("versions is empty"); - } - - let mut keyed_versions = versions - .iter() - .map(|version| Ok((normalize_version(version)?, version.clone()))) - .collect::>>()?; - keyed_versions.sort_by(|left, right| left.0.cmp(&right.0).then(left.1.cmp(&right.1))); - - let mut ordered_versions = Vec::with_capacity(keyed_versions.len()); - let mut previous: Option = None; - for (normalized, original) in keyed_versions { - if previous.as_ref() == Some(&normalized) { - bail!("duplicate version '{original}'"); - } - previous = Some(normalized); - ordered_versions.push(original); - } - Ok(ordered_versions) -} - -fn load_publisher_set(commit_info_file: &PathBuf) -> Result<(Vec, u16)> { - let bytes = std::fs::read(commit_info_file).with_context(|| { - format!("failed to read commit_info file '{}'", commit_info_file.display()) - })?; - let commit_info: CommitInfo = serde_json::from_slice(&bytes).with_context(|| { - format!("failed to decode commit_info file '{}'", commit_info_file.display()) - })?; - let (encoded_public_keys, threshold) = commit_info.active_publisher_set(); - if encoded_public_keys.is_empty() { - bail!("commit_info publisher_public_keys is empty"); - } - if threshold == 0 { - bail!("commit_info threshold must be greater than 0"); - } - let publisher_public_keys = encoded_public_keys - .iter() - .map(|compressed_pk| { - PublicKey::from_str(compressed_pk) - .with_context(|| format!("invalid publisher public key '{}'", compressed_pk)) - }) - .collect::>>()?; - if usize::from(threshold) > publisher_public_keys.len() { - bail!( - "commit_info threshold {} exceeds publisher_public_keys length {}", - threshold, - publisher_public_keys.len() - ); - } - Ok((publisher_public_keys, threshold)) -} - -fn load_part_stark_vk_for_version(zkm_version: &str) -> Result> { - std::panic::catch_unwind(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec()) - .map_err(|_| anyhow::anyhow!("failed to load part_stark_vk for version '{zkm_version}'")) -} - -fn load_part_stark_vks_for_versions(ordered_versions: &[String]) -> Result>> { - ordered_versions.iter().map(|version| load_part_stark_vk_for_version(version)).collect() -} - -fn decode_publisher_secret_key_wif(value: &str) -> Result { - let private_key = - PrivateKey::from_wif(value).map_err(|err| format!("invalid publisher WIF key: {err}"))?; - Ok(private_key.inner) -} - -/// Resolve the signer index by matching the secret key derived public key against the active set. -fn resolve_signer_index( - secret_key: &SecretKey, - publisher_public_keys: &[PublicKey], -) -> Result { - let signer_public_key = PublicKey::from_secret_key(&Secp256k1::new(), secret_key); - publisher_public_keys.iter().position(|public_key| *public_key == signer_public_key).ok_or_else( - || anyhow::anyhow!("publisher_secret_key_wif does not belong to active publisher set"), - ) -} - -fn main() -> Result<()> { - dotenv::dotenv().ok(); - let cli = Cli::parse(); - - match cli.command { - Command::BuildTree { versions, attestation_dir } => { - let attestation_dir = resolve_attestation_dir(attestation_dir); - let ordered_versions = order_versions(&versions)?; - let part_stark_vks = load_part_stark_vks_for_versions(&ordered_versions)?; - save_latest_part_stark_vk_attestation_snapshot( - &attestation_dir, - &ordered_versions, - &part_stark_vks, - None, - None, - None, - vec![], - ) - .map_err(anyhow::Error::msg)?; - println!( - "built latest part_stark_vk snapshot in {} for versions {}", - attestation_dir.display(), - ordered_versions.join(",") - ); - } - Command::SignRoot { - versions, - commit_info_file, - publisher_secret_key_wif, - attestation_dir, - } => { - let attestation_dir = resolve_attestation_dir(attestation_dir); - let ordered_versions = order_versions(&versions)?; - let part_stark_vks = load_part_stark_vks_for_versions(&ordered_versions)?; - let (publisher_public_keys, threshold) = load_publisher_set(&commit_info_file)?; - let signer_pubkey_index = - resolve_signer_index(&publisher_secret_key_wif, &publisher_public_keys)?; - let manifest = sign_latest_part_stark_vk_snapshot( - &attestation_dir, - &ordered_versions, - &part_stark_vks, - &publisher_public_keys, - threshold, - signer_pubkey_index, - &publisher_secret_key_wif, - ) - .map_err(anyhow::Error::msg)?; - println!("{}", serde_json::to_string_pretty(&manifest)?); - } - } - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use bitcoin::Network; - use bitcoin::secp256k1::Secp256k1; - - #[test] - fn test_decode_publisher_secret_key_wif_accepts_wif() { - let secret_key = - SecretKey::from_str("0101010101010101010101010101010101010101010101010101010101010101") - .unwrap(); - let private_key = PrivateKey::new(secret_key, Network::Regtest); - - let decoded = decode_publisher_secret_key_wif(&private_key.to_wif()).unwrap(); - - assert_eq!(decoded, secret_key); - } - - #[test] - fn test_order_versions_sorts_semver_with_v_prefix() { - let ordered = - order_versions(&["v1.2.10".to_string(), "v1.2.4".to_string(), "v1.2.5".to_string()]) - .unwrap(); - assert_eq!(ordered, vec!["v1.2.4", "v1.2.5", "v1.2.10"]); - } - - #[test] - fn test_order_versions_rejects_duplicate_versions() { - let err = order_versions(&["v1.2.5".to_string(), "v1.2.5".to_string()]).unwrap_err(); - assert!(err.to_string().contains("duplicate")); - } - - #[test] - fn test_resolve_signer_index_matches_active_publisher_set_order() { - let secp = Secp256k1::new(); - let secret_key_0 = - SecretKey::from_str("0101010101010101010101010101010101010101010101010101010101010101") - .unwrap(); - let secret_key_1 = - SecretKey::from_str("0202020202020202020202020202020202020202020202020202020202020202") - .unwrap(); - let secret_key_2 = - SecretKey::from_str("0303030303030303030303030303030303030303030303030303030303030303") - .unwrap(); - let publisher_public_keys = vec![ - PublicKey::from_secret_key(&secp, &secret_key_2), - PublicKey::from_secret_key(&secp, &secret_key_0), - PublicKey::from_secret_key(&secp, &secret_key_1), - ]; - - let signer_index = resolve_signer_index(&secret_key_0, &publisher_public_keys).unwrap(); - - assert_eq!(signer_index, 1); - } -} diff --git a/node/src/bin/send_bridge_out.rs b/node/src/bin/send_bridge_out.rs index d585c559..583727ab 100644 --- a/node/src/bin/send_bridge_out.rs +++ b/node/src/bin/send_bridge_out.rs @@ -12,7 +12,7 @@ use alloy::eips::BlockNumberOrTag; use alloy::primitives::{Address as EvmAddress, Bytes, U256}; use alloy::providers::{Provider, ProviderBuilder}; use anyhow::{Context, Result, anyhow, bail}; -use bitvm2_noded::env::{ +use bitvm_noded::env::{ ENV_GOAT_PRIVATE_KEY, ENV_GOAT_SWAP_CONTRACT_ADDRESS, get_goat_network, goat_config_from_env, }; use clap::{Parser, Subcommand}; diff --git a/node/src/bin/send_challenge.rs b/node/src/bin/send_challenge.rs index e70b72ca..31abeb4c 100644 --- a/node/src/bin/send_challenge.rs +++ b/node/src/bin/send_challenge.rs @@ -12,13 +12,13 @@ //! - --graph-id: target graph UUID //! //! Example: -//! - cargo run -p bitvm2-noded --bin challenge -- \ +//! - cargo run -p bitvm-noded --bin challenge -- \ //! --rpc-url http://localhost:8080 \ //! --graph-id use anyhow::{Context, Result}; -use bitvm2_noded::env::get_bitvm_key; -use bitvm2_noded::rpc_service::auth::{ +use bitvm_noded::env::get_bitvm_key; +use bitvm_noded::rpc_service::auth::{ AUTH_SIGNATURE_HEADER, AUTH_TIMESTAMP_HEADER, sign_request_auth, }; use clap::Parser; diff --git a/node/src/bin/send_pegin_request.rs b/node/src/bin/send_pegin_request.rs index a8da75e0..354af0d0 100644 --- a/node/src/bin/send_pegin_request.rs +++ b/node/src/bin/send_pegin_request.rs @@ -17,7 +17,7 @@ //! - Most args are optional; use --help for the full list. //! //! Example: -//! - cargo run -p bitvm2-noded --bin pegin-request -- request-prepare +//! - cargo run -p bitvm-noded --bin pegin-request -- request-prepare use alloy::primitives::Address as EvmAddress; use alloy::providers::{Provider, ProviderBuilder}; @@ -26,7 +26,7 @@ use bitcoin::hashes::Hash; use bitcoin::sighash::EcdsaSighashType; use bitcoin::{Address, Amount, key::Keypair}; use bitcoin::{Network, TapSighashType, XOnlyPublicKey}; -use bitvm2_lib::types::Bitvm2InstanceParameters; +use bitvm_lib::types::BitvmGcInstanceParameters; use clap::{Parser, Subcommand}; use client::btc_chain::BTCClient; use client::goat_chain::utils::get_gateway_relay_contracts; @@ -42,11 +42,11 @@ use std::time::Duration; use tokio::time::sleep; use tracing_subscriber::EnvFilter; -use bitvm2_noded::env::{ +use bitvm_noded::env::{ ENV_BITCOIN_NETWORK, ENV_BITVM_SECRET, ENV_GOAT_ADDRESS, get_goat_network, get_node_goat_address, goat_config_from_env, }; -use bitvm2_noded::utils::{ +use bitvm_noded::utils::{ broadcast_tx, get_fee_rate, get_proper_utxo_set, node_p2wsh_address, node_sign, }; @@ -355,8 +355,8 @@ async fn action_prepare( }; let user_keypair = Keypair::from_seckey_str_global(user_btc_secret)?; - let instance_params: Bitvm2InstanceParameters = - bitvm2_noded::utils::read_instance_info_from_goat(goat_client, instance_id).await?; + let instance_params: BitvmGcInstanceParameters = + bitvm_noded::utils::read_instance_info_from_goat(goat_client, instance_id).await?; // Build pegin deposit/confirm/refund transactions let (mut pegin_deposit, _confirm, _refund) = instance_params.build_pegin_tx()?; @@ -392,8 +392,8 @@ async fn action_cancel( let user_keypair = Keypair::from_seckey_str_global(user_btc_secret)?; let user_taproot_public_key = user_keypair.public_key().x_only_public_key().0; - let instance_params: Bitvm2InstanceParameters = - bitvm2_noded::utils::read_instance_info_from_goat(goat_client, instance_id).await?; + let instance_params: BitvmGcInstanceParameters = + bitvm_noded::utils::read_instance_info_from_goat(goat_client, instance_id).await?; let n_of_n_taproot_public_key = XOnlyPublicKey::from(instance_params.committee_agg_pubkey); // Build pegin deposit/confirm/refund transactions and pick the refund (cancel) diff --git a/node/src/bin/send_pegout.rs b/node/src/bin/send_pegout.rs index 67fed93e..77e9270d 100644 --- a/node/src/bin/send_pegout.rs +++ b/node/src/bin/send_pegout.rs @@ -11,12 +11,12 @@ //! - --rpc-url: node API base URL (default: http://localhost:8080) //! //! Example: -//! - cargo run -p bitvm2-noded --bin pegout -- \ +//! - cargo run -p bitvm-noded --bin pegout -- \ //! --rpc-url http://localhost:8080 once --graph-id use anyhow::{Context, Result, bail}; -use bitvm2_noded::env::get_bitvm_key; -use bitvm2_noded::rpc_service::auth::{ +use bitvm_noded::env::get_bitvm_key; +use bitvm_noded::rpc_service::auth::{ AUTH_SIGNATURE_HEADER, AUTH_TIMESTAMP_HEADER, sign_request_auth, }; use clap::{Parser, Subcommand}; diff --git a/node/src/bin/send_rbf.rs b/node/src/bin/send_rbf.rs index 05497dbf..861a37f5 100644 --- a/node/src/bin/send_rbf.rs +++ b/node/src/bin/send_rbf.rs @@ -10,7 +10,7 @@ //! - BITCOIN_NETWORK: bitcoin | testnet | testnet4 | signet | regtest (optional) //! //! Example: -//! - cargo run -p bitvm2-noded --bin send-rbf -- \ +//! - cargo run -p bitvm-noded --bin send-rbf -- \ //! --vin :0 \ //! --vin :1 \ //! --fee-amount 10000 \ @@ -31,8 +31,8 @@ use dotenv::dotenv; use goat::transactions::base::Input; use tracing_subscriber::EnvFilter; -use bitvm2_noded::env::{DUST_AMOUNT, get_bitvm_key, get_network}; -use bitvm2_noded::utils::{broadcast_tx, node_p2wsh_address, node_sign}; +use bitvm_noded::env::{DUST_AMOUNT, get_bitvm_key, get_network}; +use bitvm_noded::utils::{broadcast_tx, node_p2wsh_address, node_sign}; const DEFAULT_RBF_SEQUENCE: u32 = 0xFFFF_FFFD; diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index c6fe4f6c..1a8d2caf 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -11,13 +11,13 @@ use bitcoin::{ Address, Amount, OutPoint, PrivateKey, PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, absolute::LockTime, hashes::Hash, key::Keypair, transaction::Version, }; -use bitvm2_noded::env::{ +use bitvm_noded::env::{ ENV_GOAT_SEQUENCER_SET_MULTI_SIG_VERIFIER_ADDRESS, ENV_GOAT_SEQUENCER_SET_PUBLISHER_CONTRACT_ADDRESS, get_goat_address_from_env, get_network, }; -use bitvm2_noded::utils::wait_tx_confirmation; -use bitvm2_noded::utils::{broadcast_tx, get_fee_rate}; -use bitvm2_noded::utils::{node_p2wsh_address, node_sign}; +use bitvm_noded::utils::wait_tx_confirmation; +use bitvm_noded::utils::{broadcast_tx, get_fee_rate}; +use bitvm_noded::utils::{node_p2wsh_address, node_sign}; use clap::{Parser, Subcommand}; use client::btc_chain::BTCClient; use client::goat_chain::GOATClient; @@ -84,7 +84,7 @@ struct Args { #[arg(long, env = "OUTPUT_FILE", default_value = "output.data")] output_file: String, - #[arg(long, env = "DB_PATH", default_value = "sqlite:/tmp/bitvm2-node.db")] + #[arg(long, env = "DB_PATH", default_value = "sqlite:/tmp/bitvm-node.db")] db_path: String, } @@ -167,17 +167,25 @@ fn save_output(input: OutputData, output_file: &str) { } /// Query sequencer set hash from local DB. -/// If `goat_block_number` is provided, find the first record at or after that goat block. -/// If not provided, return the latest record. +/// If `goat_block_number` is provided, find the first record at or before that goat block. +/// That is, the sequencer set hash at that goat block. +/// If not provided, query the latest record. +/// For init-genesis, return the requested goat block number as the resolved height. +/// Otherwise, return the goat block number from the matched DB record. async fn get_sequencer_set_hash_from_db( db_path: &str, goat_block_number: Option, + init_genesis: bool, ) -> Result<([u8; 32], u64, u64), Box> { + if init_genesis && goat_block_number.is_none() { + return Err("init_genesis requires an explicit goat_block_number".into()); + } + let local_db = store::create_local_db(db_path).await; let mut storage = local_db.acquire().await?; let record = if let Some(goat_block_number) = goat_block_number { storage - .find_first_sequencer_set_hash_change_by_goat_block_at_or_after(i64::try_from( + .find_first_sequencer_set_hash_change_by_goat_block_at_or_before(i64::try_from( goat_block_number, )?) .await? @@ -188,8 +196,14 @@ async fn get_sequencer_set_hash_from_db( record.ok_or("No validators_hash record found in db. Start the monitor task first")?; let sequencer_set_hash = <[u8; 32]>::from_hex(record.validators_hash.trim_start_matches("0x"))?; - let goat_block_number = u64::try_from(record.goat_block_height)?; + let record_goat_block_number = u64::try_from(record.goat_block_height)?; + let goat_block_number = if init_genesis { + goat_block_number.ok_or("init_genesis requires an explicit goat_block_number")? + } else { + record_goat_block_number + }; let cosmos_block_number = u64::try_from(record.cosmos_block_height)?; + Ok((sequencer_set_hash, goat_block_number, cosmos_block_number)) } @@ -216,6 +230,8 @@ enum Commands { next_publisher_btc_pubkeys: Vec, #[arg(long, env = "GOAT_GENESIS_BLOCK_HASH", value_parser = hex_parse::<32>)] goat_genesis_block_hash: [u8; 32], + #[arg(long, env = "OPERATOR_VK_HASH", value_parser = hex_parse::<32>)] + operator_vk_hash: [u8; 32], }, PushSeq { #[arg(long, env = "OWNER_BTC_KEY_WIF")] @@ -230,6 +246,8 @@ enum Commands { init_genesis: bool, #[arg(long, env = "GOAT_GENESIS_BLOCK_HASH", value_parser = hex_parse::<32>)] goat_genesis_block_hash: [u8; 32], + #[arg(long, env = "OPERATOR_VK_HASH", value_parser = hex_parse::<32>)] + operator_vk_hash: [u8; 32], #[arg(long)] commit_info: String, }, @@ -320,9 +338,10 @@ async fn main() -> Result<(), Box> { publisher_btc_pubkeys, next_publisher_btc_pubkeys, goat_genesis_block_hash, + operator_vk_hash, } => { let (sequencer_set_hash, goat_block_number, cosmos_block_number) = - get_sequencer_set_hash_from_db(&args.db_path, goat_block_number).await?; + get_sequencer_set_hash_from_db(&args.db_path, goat_block_number, false).await?; println!( "resolved cl block number: {cosmos_block_number}, resolved el block number: {goat_block_number}" ); @@ -340,6 +359,7 @@ async fn main() -> Result<(), Box> { update_connector, sequencer_set_hash, goat_genesis_block_hash, + operator_vk_hash, goat_block_number, ) .await @@ -351,11 +371,13 @@ async fn main() -> Result<(), Box> { next_publisher_btc_pubkeys, init_genesis, goat_genesis_block_hash, + operator_vk_hash, commit_info, } => { println!("goat genesis block hash: {:#?}", hex::encode(goat_genesis_block_hash)); let (sequencer_set_hash, goat_block_number, cosmos_block_number) = - get_sequencer_set_hash_from_db(&args.db_path, goat_block_number).await?; + get_sequencer_set_hash_from_db(&args.db_path, goat_block_number, init_genesis) + .await?; let sequencers = fetch_validators(&args.cosmos_rpc_url, cosmos_block_number).await?; let fee_tx = cached_output.fee_tx; @@ -372,6 +394,7 @@ async fn main() -> Result<(), Box> { goat_block_number, sequencer_set_hash, goat_genesis_block_hash, + operator_vk_hash, output_file, ) .await?; @@ -524,6 +547,7 @@ async fn action_push_sequencer_set_update( goat_block_number: u64, sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], + operator_vk_hash: [u8; 32], output_file: &str, ) -> Result<(), Box> { let witnesses = goat_client.ss_get_sequencer_set_update_witness(goat_block_number).await?; @@ -584,9 +608,10 @@ async fn action_push_sequencer_set_update( }; // Skip construction of the genesis tx - let mut commitment = [0u8; 64]; + let mut commitment = [0u8; 96]; commitment[0..32].copy_from_slice(&sequencer_set_hash); - commitment[32..].copy_from_slice(&goat_genesis_block_hash[0..32]); + commitment[32..64].copy_from_slice(&goat_genesis_block_hash); + commitment[64..].copy_from_slice(&operator_vk_hash); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, &update_connector, @@ -632,6 +657,7 @@ async fn action_sign_sequencer_set_update( update_connector: Option, sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], + operator_vk_hash: [u8; 32], goat_block_number: u64, ) -> Result<(), Box> { let total = btc_public_keys.len(); @@ -648,9 +674,10 @@ async fn action_sign_sequencer_set_update( * estimate_tx_vbytes(&[(threshold as u32, total as u32)], &[("p2wsh", 3)], 73) as f64 + RELAYER_FEE as f64; let replenish_fee = Amount::from_sat(replenish_fee.ceil() as u64); - let mut commitment = [0u8; 64]; + let mut commitment = [0u8; 96]; commitment[0..32].copy_from_slice(&sequencer_set_hash); - commitment[32..].copy_from_slice(&goat_genesis_block_hash[0..32]); + commitment[32..64].copy_from_slice(&goat_genesis_block_hash); + commitment[64..].copy_from_slice(&operator_vk_hash); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, diff --git a/node/src/env.rs b/node/src/env.rs index dd2b8877..b923552d 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -6,8 +6,8 @@ use alloy::providers::{Provider, ProviderBuilder}; use alloy::signers::local::PrivateKeySigner; use base64::Engine; use bitcoin::{Network, PublicKey, key::Keypair}; -use bitvm2_lib::actors::Actor; -use bitvm2_lib::keys::NodeMasterKey; +use bitvm_lib::actors::Actor; +use bitvm_lib::keys::NodeMasterKey; use client::goat_chain::utils::{ get_committee_management_contract, get_gateway_relay_contracts, is_validate_committee, }; @@ -16,9 +16,11 @@ use goat::constants::{CONNECTOR_Z_TIMELOCK, NUM_BLOCKS_PER_HOUR}; use libp2p::PeerId; use reqwest::Url; use sha2::{Digest, Sha256}; +use std::path::PathBuf; use std::str::FromStr; use strum::{Display, EnumString}; use tracing::{info, warn}; +use util::hex_parse; use zeroize::Zeroizing; pub const ENV_BTC_CHAIN_URL: &str = "BTC_CHAIN_URL"; @@ -76,8 +78,15 @@ pub const ENV_GOAT_NETWORK: &str = "GOAT_NETWORK"; pub const ENV_WATCHTOWER_PROOF_WAIT_SECS: &str = "WATCHTOWER_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_PROOF_WAIT_SECS: &str = "OPERATOR_PROOF_WAIT_SECS"; +pub const ENV_OPERATOR_VK_HASH: &str = "OPERATOR_VK_HASH"; +pub const ENV_OPERATOR_WRAPPER_VK_HASH: &str = "OPERATOR_WRAPPER_VK_HASH"; +pub const ENV_OPERATOR_WRAPPER_ZKM_VERSION: &str = "OPERATOR_WRAPPER_ZKM_VERSION"; pub const DEFAULT_WATCHTOWER_PROOF_WAIT_SECS: usize = 60; pub const DEFAULT_OPERATOR_PROOF_WAIT_SECS: usize = 60; +pub const ENV_GC_GATES_PATH: &str = "GC_GATES_PATH"; +pub const ENV_GC_INDICES_PATH: &str = "GC_INDICES_PATH"; +pub const ENV_BABE_SETUP_STATE_DIR: &str = "BABE_SETUP_STATE_DIR"; +pub const ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH: &str = "SOLDERING_PROOF_PAYLOAD_STORE_PATH"; pub const ENV_ALWAYS_CHALLENGE: &str = "ALWAYS_CHALLENGE"; pub const ENV_GENESIS_SEQUENCER_COMMIT_TXID: &str = "GENESIS_SEQUENCER_COMMIT_TXID"; @@ -193,8 +202,8 @@ pub fn get_node_pubkey() -> anyhow::Result { } pub fn get_actor() -> Actor { - Actor::from_str(std::env::var(ENV_ACTOR).unwrap_or("Challenger".to_string()).as_str()) - .expect("Expect one of Committee, Challenger, Operator or Relayer") + Actor::from_str(std::env::var(ENV_ACTOR).unwrap_or("Verifier".to_string()).as_str()) + .expect("Expect one of Committee, Verifier, Operator or Relayer") } pub fn get_peer_key() -> String { @@ -260,10 +269,10 @@ pub async fn check_node_info() { panic!("Relayer and Committee must set goat secret key"); } let node_info = get_local_node_info(); - if [Actor::Operator.to_string(), Actor::Challenger.to_string()].contains(&node_info.actor) + if [Actor::Operator.to_string(), Actor::Verifier.to_string()].contains(&node_info.actor) && node_info.goat_addr.is_empty() { - panic!("Operator and Challenger must set goat address or goat secret key"); + panic!("Operator and Verifier must set goat address or goat secret key"); } if Actor::Committee.to_string() == node_info.actor || Actor::Operator.to_string() == node_info.actor @@ -333,24 +342,34 @@ pub fn get_committee_member_num() -> usize { COMMITTEE_MEMBER_NUMBER } -#[derive(Clone, Display, EnumString)] +#[derive(Clone, Copy, Display, EnumString)] pub enum GraphBtcTxName { - #[strum(serialize = "watchtower-challenge-init.hex")] - WatchtowerChallengeInit, - #[strum(serialize = "pre-kickoff.hex")] - PreKickoff, - #[strum(serialize = "assert-init.hex")] - AssertInit, - #[strum(serialize = "challenge.hex")] - Challenge, - #[strum(serialize = "disprove.hex")] - Disprove, - #[strum(serialize = "kickoff.hex")] - Kickoff, + #[strum(serialize = "cur-pre-kickoff.hex")] + CurPreKickoff, + #[strum(serialize = "next-pre-kickoff.hex")] + NextPreKickoff, + #[strum(serialize = "force-skip-kickoff.hex")] + ForceSkipKickoff, + #[strum(serialize = "quick-challenge.hex")] + QuickChallenge, + #[strum(serialize = "challenge-incomplete-kickoff.hex")] + ChallengeIncompleteKickoff, #[strum(serialize = "pegin.hex")] Pegin, + #[strum(serialize = "kickoff.hex")] + Kickoff, #[strum(serialize = "take1.hex")] Take1, + #[strum(serialize = "challenge.hex")] + Challenge, + #[strum(serialize = "watchtower-challenge-init.hex")] + WatchtowerChallengeInit, + #[strum(serialize = "operator-assert.hex")] + OperatorAssert, + #[strum(serialize = "verifier-assert.hex")] + VerifierAssert, + #[strum(serialize = "disprove.hex")] + Disprove, #[strum(serialize = "take2.hex")] Take2, } @@ -430,13 +449,12 @@ pub fn get_goat_swap_event_filter_gap_from_env() -> i64 { pub fn get_goat_gateway_the_graph_urls_from_env() -> String { std::env::var(ENV_GOAT_GATEWAY_EVENT_THE_GRAPH_URL) - .unwrap_or("https://graph.goat.network/subgraphs/name/bitvm2_gateway_dev".to_string()) + .unwrap_or("https://graph.goat.network/subgraphs/name/bitvm_gateway_dev".to_string()) } pub fn get_goat_swap_the_graph_urls_from_env() -> String { - std::env::var(ENV_GOAT_SWAP_EVENT_THE_GRAPH_URL).unwrap_or( - "https://graph.goat.network/subgraphs/name/bitvm2_escrow_manager_dev".to_string(), - ) + std::env::var(ENV_GOAT_SWAP_EVENT_THE_GRAPH_URL) + .unwrap_or("https://graph.goat.network/subgraphs/name/bitvm_escrow_manager_dev".to_string()) } pub async fn goat_config_from_env() -> GoatInitConfig { @@ -503,7 +521,7 @@ pub async fn goat_config_from_env() -> GoatInitConfig { } } -const DEFAULT_PROTO_NAME_BASE: &str = "bitvm2"; +const DEFAULT_PROTO_NAME_BASE: &str = "bitvm"; pub fn get_proto_base() -> String { match std::env::var("PROTO_NAME") { Ok(proto_name) => { @@ -557,6 +575,61 @@ pub fn get_operator_proof_wait_secs() -> usize { .unwrap_or(DEFAULT_OPERATOR_PROOF_WAIT_SECS) } +pub fn get_soldering_proof_payload_store_path() -> anyhow::Result { + let value = std::env::var(ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH) + .map_err(|_| anyhow::anyhow!("{ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH} needs to be set"))?; + let value = value.trim(); + if value.is_empty() { + anyhow::bail!("{ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH} cannot be empty"); + } + Ok(value.to_string()) +} + +pub fn validate_soldering_proof_payload_store_config(actor: &Actor) -> anyhow::Result<()> { + if matches!(actor, Actor::Verifier | Actor::Operator | Actor::All) { + get_soldering_proof_payload_store_path() + .map(|_| ()) + .map_err(|err| anyhow::anyhow!("{err}; required for actor {actor}")) + } else { + Ok(()) + } +} + +pub fn get_operator_vk_hash() -> anyhow::Result<[u8; 32]> { + let value = std::env::var(ENV_OPERATOR_VK_HASH) + .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_VK_HASH} needs to be set"))?; + hex_parse::<32>(&value).map_err(|err| anyhow::anyhow!("invalid {ENV_OPERATOR_VK_HASH}: {err}")) +} + +pub fn get_operator_wrapper_vk_hash() -> anyhow::Result { + std::env::var(ENV_OPERATOR_WRAPPER_VK_HASH) + .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_VK_HASH} needs to be set")) +} + +pub fn get_operator_wrapper_zkm_version() -> anyhow::Result { + std::env::var(ENV_OPERATOR_WRAPPER_ZKM_VERSION) + .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_ZKM_VERSION} needs to be set")) +} + +/// Returns the configured GC asset paths after checking that they are readable files. +/// TODO: maybe multi files +pub fn get_babe_gc_asset_paths() -> anyhow::Result<(PathBuf, PathBuf)> { + let gates_path = PathBuf::from( + std::env::var(ENV_GC_GATES_PATH) + .map_err(|_| anyhow::anyhow!("{ENV_GC_GATES_PATH} is missing"))?, + ); + let indices_path = PathBuf::from( + std::env::var(ENV_GC_INDICES_PATH) + .map_err(|_| anyhow::anyhow!("{ENV_GC_INDICES_PATH} is missing"))?, + ); + for (name, path) in [(ENV_GC_GATES_PATH, &gates_path), (ENV_GC_INDICES_PATH, &indices_path)] { + if !path.is_file() { + anyhow::bail!("{name} does not point to a readable file: {}", path.display()); + } + } + Ok((gates_path, indices_path)) +} + pub fn get_instance_maintenance_batch_size() -> u32 { std::env::var(ENV_INSTANCE_MAINTENANCE_BATCH_SIZE) .ok() @@ -606,3 +679,33 @@ pub fn get_genesis_sequencer_commit_id() -> [u8; 32] { let txid = bitcoin::Txid::from_str(&hexed).expect("Invalid genesis sequencer commit txid"); txid.to_byte_array() } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Mutex; + + static ENV_LOCK: Mutex<()> = Mutex::new(()); + + #[test] + fn soldering_proof_payload_store_config_required_for_soldering_actors() { + let _guard = ENV_LOCK.lock().unwrap(); + unsafe { std::env::remove_var(ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH) }; + + assert!(validate_soldering_proof_payload_store_config(&Actor::Verifier).is_err()); + assert!(validate_soldering_proof_payload_store_config(&Actor::Operator).is_err()); + assert!(validate_soldering_proof_payload_store_config(&Actor::All).is_err()); + assert!(validate_soldering_proof_payload_store_config(&Actor::Committee).is_ok()); + assert!(validate_soldering_proof_payload_store_config(&Actor::Watchtower).is_ok()); + assert!(validate_soldering_proof_payload_store_config(&Actor::Publisher).is_ok()); + + unsafe { std::env::set_var(ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH, "/tmp/store") }; + assert!(validate_soldering_proof_payload_store_config(&Actor::Verifier).is_ok()); + assert!(validate_soldering_proof_payload_store_config(&Actor::Operator).is_ok()); + + unsafe { std::env::set_var(ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH, " ") }; + assert!(validate_soldering_proof_payload_store_config(&Actor::Verifier).is_err()); + + unsafe { std::env::remove_var(ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH) }; + } +} diff --git a/node/src/handle.rs b/node/src/handle.rs index 25f644b7..eb2f385c 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -1,30 +1,42 @@ use crate::action::*; use crate::env::{ - COMMITTEE_INSTANCE_KEYS_DIR, get_bitvm_key, get_network, get_node_goat_address, is_relayer, + COMMITTEE_INSTANCE_KEYS_DIR, get_babe_gc_asset_paths, get_bitvm_key, get_node_goat_address, + get_soldering_proof_payload_store_path, is_relayer, }; use crate::error::SpecialError; use crate::middleware::AllBehaviours; use crate::scheduled_tasks::graph_maintenance_tasks::ChallengeSubStatus; +use crate::soldering_payload_store::{ + read_soldering_proof_store_payload, soldering_proof_payload_store_path, +}; use crate::utils::*; use anyhow::{Context, Result, anyhow, bail}; -use bitcoin::hashes::Hash; -use bitcoin::{OutPoint, Txid}; +use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; -use bitvm2_lib::actors::Actor; -use bitvm2_lib::challenger::*; -use bitvm2_lib::committee::*; -use bitvm2_lib::keys::*; -use bitvm2_lib::operator::*; -use bitvm2_lib::types::{Bitvm2Graph, SimplifiedBitvm2Graph}; +use bitvm_lib::actors::Actor; +use bitvm_lib::babe_adapter::{ + BABE_M_CC, BABE_N_CC, BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, + BabeProverState, CACSetupPackage, CompactSolderingProofPayload, assert_wots_message, + build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, + derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, + open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, +}; +use bitvm_lib::committee::*; +use bitvm_lib::keys::*; +use bitvm_lib::operator::*; +use bitvm_lib::types::{BitvmGcCircuitData, BitvmGcGraph, SimplifiedBitvmGcGraph}; +use bitvm_lib::verifier::*; use client::goat_chain::{DisproveTxType, PeginStatus, WithdrawStatus}; use client::http_client::async_client::HttpAsyncClient; use client::{btc_chain::BTCClient, goat_chain::GOATClient}; use goat::connectors::connector_z::ConnectorZ; -use goat::transactions::base::{BaseTransaction, Input}; +use goat::transactions::base::output_topology; use goat::transactions::pre_signed::PreSignedTransaction; use goat::transactions::pre_signed_musig2::verify_public_nonce; use libp2p::gossipsub::MessageId; use libp2p::{PeerId, Swarm}; +use std::sync::Arc; use store::GraphStatus; use store::localdb::LocalDB; use uuid::Uuid; @@ -35,6 +47,7 @@ pub struct HandlerContext<'a> { pub btc_client: &'a BTCClient, pub goat_client: &'a GOATClient, pub http_client: &'a HttpAsyncClient, + pub soldering_builder: &'a Option>, pub actor: Actor, pub from_peer_id: PeerId, pub id: MessageId, @@ -158,6 +171,67 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent (GOATMessageContent::ConfirmInstance(ConfirmInstance { instance_id }), _) => { handle_confirm_instance_default(ctx, *instance_id).await } + (GOATMessageContent::InitGraph(InitGraph { instance_id, graph_id }), Actor::Verifier) => { + handle_init_graph_verifier(ctx, *instance_id, *graph_id).await + } + ( + GOATMessageContent::GenCircuits(GenCircuits { + instance_id, + graph_id, + verifier_pubkey, + setup_package, + }), + Actor::Operator, + ) => { + handle_gen_circuits_operator( + ctx, + *instance_id, + *graph_id, + verifier_pubkey, + setup_package, + ) + .await + } + ( + GOATMessageContent::CutCircuits(CutCircuits { + instance_id, + graph_id, + verifier_pubkey, + verifier_index, + selected_circuit_indexes, + }), + Actor::Verifier, + ) => { + handle_cut_circuits_verifier( + ctx, + *instance_id, + *graph_id, + verifier_pubkey, + *verifier_index, + selected_circuit_indexes, + ) + .await + } + ( + GOATMessageContent::SolderingProofReady(SolderingProofReady { + instance_id, + graph_id, + verifier_index, + payload_hash, + total_len, + }), + Actor::Operator, + ) => { + handle_soldering_proof_ready_operator( + ctx, + *instance_id, + *graph_id, + *verifier_index, + *payload_hash, + *total_len, + ) + .await + } ( GOATMessageContent::CreateGraph(CreateGraph { instance_id, graph_id, graph, .. }), Actor::Committee, @@ -167,8 +241,6 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent instance_id, graph_id, committee_pubkey: received_committee_pubkey, - watchtower_num, - assert_commit_num, pub_nonces, nonce_sigs, }), @@ -179,8 +251,6 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent *instance_id, *graph_id, received_committee_pubkey, - *watchtower_num, - *assert_commit_num, pub_nonces, nonce_sigs, content, @@ -192,8 +262,6 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent instance_id, graph_id, committee_pubkey: received_committee_pubkey, - watchtower_num, - assert_commit_num, pub_nonces, nonce_sigs, }), @@ -204,8 +272,6 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent *instance_id, *graph_id, received_committee_pubkey, - *watchtower_num, - *assert_commit_num, pub_nonces, nonce_sigs, ) @@ -343,15 +409,15 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent ) => handle_kickoff_sent_committee(ctx, *instance_id, *graph_id, content).await, ( GOATMessageContent::KickoffSent(KickoffSent { instance_id, graph_id }), - Actor::Challenger, - ) => handle_kickoff_sent_challenger(ctx, *instance_id, *graph_id, content).await, + Actor::Verifier, + ) => handle_kickoff_sent_verifier(ctx, *instance_id, *graph_id, content).await, (GOATMessageContent::KickoffSent(KickoffSent { instance_id, graph_id }), _) => { handle_kickoff_sent_default(ctx, *instance_id, *graph_id, content).await } ( GOATMessageContent::PreKickoffSent(PreKickoffSent { instance_id, graph_id }), - Actor::Challenger, - ) => handle_prekickoff_sent_challenger(ctx, *instance_id, *graph_id, content).await, + Actor::Verifier, + ) => handle_prekickoff_sent_verifier(ctx, *instance_id, *graph_id, content).await, (GOATMessageContent::PreKickoffSent(PreKickoffSent { instance_id, graph_id }), _) => { handle_prekickoff_sent_default(ctx, *instance_id, *graph_id).await } @@ -383,7 +449,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent GOATMessageContent::WatchtowerChallengeSent(WatchtowerChallengeSent { instance_id, graph_id, - watchtower_challenge_txids, + watchtower_index, }), Actor::Operator, ) => { @@ -391,7 +457,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent ctx, *instance_id, *graph_id, - watchtower_challenge_txids, + *watchtower_index, content, ) .await @@ -400,60 +466,92 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent GOATMessageContent::WatchtowerChallengeTimeout(WatchtowerChallengeTimeout { instance_id, graph_id, - watchtower_indexes, }), Actor::Operator, ) => { - handle_watchtower_challenge_timeout_operator( - ctx, - *instance_id, - *graph_id, - watchtower_indexes, - content, - ) - .await + handle_watchtower_challenge_timeout_operator(ctx, *instance_id, *graph_id, content) + .await + } + (GOATMessageContent::NackReady(NackReady { instance_id, graph_id }), Actor::Verifier) => { + handle_nack_ready_verifier(ctx, *instance_id, *graph_id, content).await } ( - GOATMessageContent::OperatorAckTimeout(OperatorAckTimeout { instance_id, graph_id }), - Actor::Challenger, - ) => handle_operator_ack_timeout_challenger(ctx, *instance_id, *graph_id, content).await, + GOATMessageContent::OperatorCommitPubinReady(OperatorCommitPubinReady { + instance_id, + graph_id, + }), + Actor::Operator, + ) => { + handle_operator_commit_pubin_ready_operator(ctx, *instance_id, *graph_id, content).await + } ( - GOATMessageContent::OperatorCommitBlockHashReady(OperatorCommitBlockHashReady { + GOATMessageContent::OperatorCommitPubinTimeout(OperatorCommitPubinTimeout { instance_id, graph_id, }), + Actor::Verifier, + ) => { + handle_operator_commit_pubin_timeout_verifier(ctx, *instance_id, *graph_id, content) + .await + } + ( + GOATMessageContent::AssertReady(AssertReady { instance_id, graph_id }), Actor::Operator, + ) => handle_assert_ready_operator(ctx, *instance_id, *graph_id).await, + ( + GOATMessageContent::AssertSent(AssertSent { + instance_id, + graph_id, + assert_txid, + assert_witness, + }), + Actor::Verifier, ) => { - handle_operator_commit_blockhash_ready_operator(ctx, *instance_id, *graph_id, content) + handle_assert_sent_verifier(ctx, *instance_id, *graph_id, *assert_txid, assert_witness) .await } ( - GOATMessageContent::OperatorCommitBlockHashTimeout(OperatorCommitBlockHashTimeout { + GOATMessageContent::ChallengeAssertSent(ChallengeAssertSent { instance_id, graph_id, + challenge_assert_txid, + verifier_index, + challenge_witness, + .. }), - Actor::Challenger, + Actor::Operator, ) => { - handle_operator_commit_blockhash_timeout_challenger( + handle_challenge_assert_sent_operator( ctx, *instance_id, *graph_id, + *challenge_assert_txid, + *verifier_index, + challenge_witness, content, ) .await } ( - GOATMessageContent::AssertInitReady(AssertInitReady { instance_id, graph_id }), - Actor::Operator, - ) => handle_assert_init_ready_operator(ctx, *instance_id, *graph_id, content).await, - ( - GOATMessageContent::AssertCommitTimeout(AssertCommitTimeout { instance_id, graph_id }), - Actor::Challenger, - ) => handle_assert_commit_timeout_challenger(ctx, *instance_id, *graph_id, content).await, - ( - GOATMessageContent::DisproveReady(DisproveReady { instance_id, graph_id }), - Actor::Challenger, - ) => handle_disprove_ready_challenger(ctx, *instance_id, *graph_id, content).await, + GOATMessageContent::WronglyChallengeTimeout(WronglyChallengeTimeout { + instance_id, + graph_id, + challenge_assert_txid, + verifier_index, + .. + }), + Actor::Verifier, + ) => { + handle_wrongly_challenge_timeout_verifier( + ctx, + *instance_id, + *graph_id, + *challenge_assert_txid, + *verifier_index, + content, + ) + .await + } ( GOATMessageContent::DisproveSent(DisproveSent { instance_id, @@ -518,6 +616,190 @@ fn make_message(ctx: &HandlerContext<'_>, content: &GOATMessageContent) -> GOATM GOATMessage::new(ctx.actor.clone(), content.clone()) } +/// Freezes the first accepted Verifiers and assigns deterministic graph slots. +fn freeze_operator_candidates(state: &mut OperatorBabeSetupState) -> Result<()> { + if state.frozen_verifier_pubkeys.is_some() { + return Ok(()); + } + if state.candidates.len() < todo_funcs::min_required_verifier() { + bail!( + "cannot freeze {} verifier candidates before reaching target {}", + state.candidates.len(), + todo_funcs::min_required_verifier() + ); + } + state.candidates.truncate(todo_funcs::min_required_verifier()); + state.candidates.sort_by_key(|candidate| candidate.verifier_pubkey.to_bytes()); + for (verifier_index, candidate) in state.candidates.iter_mut().enumerate() { + candidate.verifier_index = Some(verifier_index); + candidate.selected_circuit_indexes = + derive_finalized_indices(&candidate.setup_package, BABE_M_CC)?; + } + state.frozen_verifier_pubkeys = + Some(state.candidates.iter().map(|candidate| candidate.verifier_pubkey).collect()); + Ok(()) +} + +/// Stores one selected Verifier slot and emits ordered graph data when complete. +fn record_candidate_gc_data( + state: &mut OperatorBabeSetupState, + verifier_pubkey: PublicKey, + verifier_index: usize, + setup_package: &CACSetupPackage, + gc_data: BitvmGcCircuitData, + prover_state: BabeProverState, +) -> Result>> { + let frozen = state + .frozen_verifier_pubkeys + .as_ref() + .ok_or_else(|| anyhow!("operator verifier membership is not frozen"))?; + let expected_pubkey = frozen + .get(verifier_index) + .ok_or_else(|| anyhow!("verifier index {verifier_index} out of frozen slot range"))?; + if expected_pubkey != &verifier_pubkey { + bail!("verifier public key does not own slot {verifier_index}"); + } + if gc_data.verifier_pubkey != verifier_pubkey { + bail!("GC slot owner does not match soldering proof verifier"); + } + let candidate = state + .candidates + .iter_mut() + .find(|candidate| candidate.verifier_pubkey == verifier_pubkey) + .ok_or_else(|| anyhow!("selected verifier candidate is missing"))?; + if candidate.verifier_index != Some(verifier_index) { + bail!("candidate verifier index does not match soldering proof slot"); + } + if candidate.setup_package != *setup_package { + bail!("soldering proof setup package does not match selected verifier candidate"); + } + if prover_state.package != *setup_package { + bail!("BABE prover state setup package does not match selected verifier candidate"); + } + if candidate.selected_circuit_indexes != prover_state.soldering.finalized_indices { + bail!("BABE prover state finalized indices do not match selected verifier cut"); + } + if prover_state.finalized.len() != BABE_M_CC || prover_state.h_msgs.len() != BABE_M_CC { + bail!("BABE prover state must contain exactly {BABE_M_CC} finalized instances and hashes"); + } + if prover_state.h_msgs != gc_data.final_msg_hashlocks { + bail!("BABE prover state hashes do not match GC slot hashes"); + } + if let Some(existing) = &candidate.gc_data + && existing != &gc_data + { + bail!("conflicting GC slot received for selected verifier"); + } + if let Some(existing) = &candidate.prover_state + && existing != &prover_state + { + bail!("conflicting BABE prover state received for selected verifier"); + } + if candidate.gc_data.is_none() { + candidate.gc_data = Some(gc_data); + } + if candidate.prover_state.is_none() { + candidate.prover_state = Some(prover_state); + } + if state.candidates.iter().any(|candidate| candidate.gc_data.is_none()) { + return Ok(None); + } + Ok(Some(state.candidates.iter().map(|candidate| candidate.gc_data.clone().unwrap()).collect())) +} + +fn validate_verifier_slot_lengths( + verifier_index: usize, + gc_data_len: usize, + verifier_asserts_len: usize, + disproves_len: usize, +) -> Result<()> { + if gc_data_len == 0 { + bail!("graph has no verifier GC slots"); + } + if gc_data_len != verifier_asserts_len || gc_data_len != disproves_len { + bail!( + "graph verifier branch lengths differ: gc_data={gc_data_len}, verifier_asserts={verifier_asserts_len}, disproves={disproves_len}" + ); + } + if verifier_index >= gc_data_len { + bail!("verifier index {verifier_index} out of range for {gc_data_len} slots"); + } + Ok(()) +} + +fn validate_verifier_slot(graph: &BitvmGcGraph, verifier_index: usize) -> Result<()> { + validate_verifier_slot_lengths( + verifier_index, + graph.parameters.gc_data.len(), + graph.verifier_asserts.len(), + graph.disproves.len(), + ) +} + +fn validate_watchtower_branches(graph: &BitvmGcGraph) -> Result { + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + if watchtower_num == 0 { + bail!("graph has no watchtower slots"); + } + if graph.parameters.watchtower_ack_hashlocks.len() != watchtower_num + || graph.watchtower_challenge_timeouts.len() != watchtower_num + || graph.operator_challenge_nacks.len() != watchtower_num + { + bail!( + "graph watchtower branch lengths differ: watchtowers={}, ack_hashlocks={}, timeouts={}, nacks={}", + watchtower_num, + graph.parameters.watchtower_ack_hashlocks.len(), + graph.watchtower_challenge_timeouts.len(), + graph.operator_challenge_nacks.len() + ); + } + Ok(watchtower_num) +} + +fn find_verifier_index_by_pubkey( + gc_data: &[BitvmGcCircuitData], + verifier_pubkey: &PublicKey, +) -> Result> { + let matches = gc_data + .iter() + .enumerate() + .filter_map(|(index, data)| (data.verifier_pubkey == *verifier_pubkey).then_some(index)) + .collect::>(); + match matches.as_slice() { + [] => Ok(None), + [index] => Ok(Some(*index)), + _ => bail!("graph contains duplicate slots for verifier {verifier_pubkey}"), + } +} + +fn validate_challenge_witness_index( + verifier_index: usize, + challenge_witness: &BabeChallengeAssertWitness, +) -> Result<()> { + if challenge_witness.verifier_index != verifier_index { + bail!( + "challenge witness verifier index {} does not match message verifier index {verifier_index}", + challenge_witness.verifier_index + ); + } + Ok(()) +} + +fn validate_expected_challenge_assert_txid( + graph: &BitvmGcGraph, + verifier_index: usize, + challenge_assert_txid: Txid, +) -> Result<()> { + validate_verifier_slot(graph, verifier_index)?; + let expected_txid = graph.verifier_asserts[verifier_index].tx().compute_txid(); + if challenge_assert_txid != expected_txid { + bail!( + "challenge assert txid {challenge_assert_txid} does not match graph verifier assert txid {expected_txid}" + ); + } + Ok(()) +} + fn should_ignore_invalid_pegin_request(e: &anyhow::Error, instance_id: Uuid) -> bool { if let Some(SpecialError::InvalidPeginRequest(err_msg)) = e.downcast_ref::() { tracing::warn!("Ignore PeginRequest for {instance_id}: {err_msg}"); @@ -648,11 +930,11 @@ async fn refresh_and_compensate( ctx: &HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - graph: Option<&Bitvm2Graph>, + graph: Option<&BitvmGcGraph>, scan_from_status: Option, compensate_from_status: GraphStatus, ) -> Result<(GraphStatus, Option)> { - let (graph_status, sub_status) = refresh_graph( + let (graph_status, sub_status, scan) = refresh_graph( ctx.local_db, ctx.btc_client, ctx.goat_client, @@ -670,10 +952,10 @@ async fn refresh_and_compensate( instance_id, graph_id, graph, + scan.as_ref(), scan_from_status, compensate_from_status, graph_status, - sub_status, ) .await?; Ok((graph_status, sub_status)) @@ -683,11 +965,11 @@ async fn get_graph_and_status( ctx: &HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, -) -> Result<(Bitvm2Graph, GraphStatus)> { +) -> Result<(BitvmGcGraph, GraphStatus)> { let graph = get_graph(ctx.local_db, instance_id, graph_id) .await? .ok_or_else(|| anyhow!("Graph not found for {instance_id}:{graph_id}"))?; - let graph = Bitvm2Graph::from_simplified(&graph)?; + let graph = BitvmGcGraph::from_simplified(&graph)?; let graph_start_status = get_graph_status(ctx.local_db, instance_id, graph_id) .await? .ok_or_else(|| anyhow!("Graph status not found for {instance_id}:{graph_id}"))?; @@ -699,7 +981,7 @@ async fn get_graph_and_status_or_defer( instance_id: Uuid, graph_id: Uuid, message: &GOATMessage, -) -> Result> { +) -> Result> { let graph = match get_graph_or_defer( ctx.swarm, ctx.local_db, @@ -713,7 +995,7 @@ async fn get_graph_and_status_or_defer( Some(g) => g, None => return Ok(None), }; - let graph = Bitvm2Graph::from_simplified(&graph)?; + let graph = BitvmGcGraph::from_simplified(&graph)?; let graph_start_status = get_graph_status(ctx.local_db, instance_id, graph_id) .await? .ok_or_else(|| anyhow!("Graph status not found for {instance_id}:{graph_id}"))?; @@ -726,7 +1008,7 @@ async fn refresh_graph_status( graph_id: Uuid, message: Option<&GOATMessage>, compensate_from_status: GraphStatus, -) -> Result)>> { +) -> Result)>> { let (graph, graph_start_status) = match message { Some(message) => { match get_graph_and_status_or_defer(ctx, instance_id, graph_id, message).await? { @@ -857,6 +1139,25 @@ async fn handle_confirm_instance_operator( send_to_peer(ctx.swarm, msg).await?; return Ok(()); } + + let pending_graph_id = { + let mut storage = ctx.local_db.acquire().await?; + storage + .find_pending_graph_init_by_instance_and_operator_pubkey( + &instance_id, + &local_operator_pubkey.to_string(), + ) + .await? + .map(|pending| pending.graph_id) + }; + if let Some(graph_id) = pending_graph_id { + tracing::info!("Resume pending graph setup for {instance_id}, graph_id: {graph_id}"); + let message_content = GOATMessageContent::InitGraph(InitGraph { instance_id, graph_id }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Verifier, message_content)).await?; + + return Ok(()); + } + // 1. read & check parameters let instance_params = match read_instance_info_from_goat(ctx.goat_client, instance_id).await { Ok(v) => v, @@ -874,100 +1175,636 @@ async fn handle_confirm_instance_operator( ); bail!("Invalid ConfirmInstance: pegin deposit tx {pegin_deposit_txid} not found on chain"); } + // after PeginPrepare is confirmed, broadcast InitGraph and let Verifiers generate GC. + // 2. save the instance data to local db store_instance_parameters(ctx.local_db, &instance_params).await?; - // 3. create & presign graph - let (graph_nonce, cur_prekickoff_tx) = - match get_current_prekickoff_tx(ctx.local_db, &local_operator_pubkey).await? { - Some(v) => v, - None => { - let genesis_prekickoff_tx = - build_genesis_prekickoff_tx(ctx.btc_client, ctx.goat_client).await?; - (0, genesis_prekickoff_tx) - } - }; - let prekickoff_params = - build_prekickoff_params(ctx.btc_client, graph_nonce, cur_prekickoff_tx).await?; - let graph_params = build_graph_params( - ctx.local_db, - ctx.goat_client, - instance_params, - prekickoff_params, - graph_nonce, - Uuid::new_v4(), - ) - .await?; - let graph_id = graph_params.graph_id; - let disprove_scripts = get_disprove_scripts(&graph_params).await?; - let mut graph = generate_bitvm_graph(graph_params, disprove_scripts)?; - operator_pre_sign(operator_master_key.master_keypair(), &mut graph)?; - store_graph(ctx.local_db, &graph.to_simplified()?).await?; - // 4. broadcast CreateGraph - let message_content = GOATMessageContent::CreateGraph(CreateGraph { + let graph_id = Uuid::new_v4(); + let mut storage = ctx.local_db.acquire().await?; + storage + .upsert_pending_graph_init(&instance_id, &local_operator_pubkey.to_string(), &graph_id) + .await?; + let message_content = GOATMessageContent::InitGraph(InitGraph { instance_id, graph_id }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Verifier, message_content)).await?; + + Ok(()) +} + +// generate garbled circuits and broadcast GenCircuits. +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_init_graph_verifier( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, +) -> Result<()> { + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let verifier_pubkey = verifier_master_key.master_keypair().public_key().into(); + + let saved_verifier_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .and_then(|state| state.verifier) + .filter(|state| state.verifier_pubkey == verifier_pubkey); + let verifier_state = if let Some(saved) = saved_verifier_state { + saved + } else { + get_babe_gc_asset_paths()?; + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE setup")?; + let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let (setup_package, private_state) = tokio::task::spawn_blocking(move || { + build_real_setup_package(BABE_N_CC, &vk, &public_inputs) + }) + .await + .context("real BABE setup task failed")??; + tracing::info!("Verifier setup done."); + VerifierBabeSetupState { + verifier_pubkey, + setup_package, + private_state, + verifier_index: None, + finalized_indices: vec![], + opened: vec![], + finalized: vec![], + soldering: None, + } + }; + + let setup_package = verifier_state.setup_package.clone(); + update_babe_setup_state(ctx.local_db, instance_id, graph_id, |state| { + state.verifier = Some(verifier_state); + })?; + + let message_content = GOATMessageContent::GenCircuits(GenCircuits { instance_id, graph_id, - graph_nonce, - graph: graph.to_simplified()?, + verifier_pubkey, + setup_package, }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Operator, message_content)).await?; + Ok(()) } -#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id))] -async fn handle_confirm_instance_default( +// select a subset of GC and broadcast CutCircuits. +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_gen_circuits_operator( ctx: &mut HandlerContext<'_>, instance_id: Uuid, + graph_id: Uuid, + verifier_pubkey: &PublicKey, + setup_package: &CACSetupPackage, ) -> Result<()> { - // triggered by PeginDeposit tx - // 1. read & check parameters - let instance_params = match read_instance_info_from_goat(ctx.goat_client, instance_id).await { - Ok(v) => v, - Err(e) => { - if should_ignore_invalid_pegin_data(&e, instance_id) { - return Ok(()); - } - bail!(e) + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let local_operator_pubkey = operator_master_key.master_keypair().public_key().into(); + if !pending_graph_belongs_to_operator( + ctx.local_db, + instance_id, + graph_id, + &local_operator_pubkey, + ) + .await? + { + tracing::debug!( + "Ignore GenCircuits for {instance_id}:{graph_id}: no local pending graph session" + ); + return Ok(()); + } + + if setup_package.commits.is_empty() { + bail!("GenCircuits setup package has no commitments"); + } + + let mut state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)?.unwrap_or_default(); + let operator_state = state.operator.get_or_insert_with(|| OperatorBabeSetupState { + frozen_verifier_pubkeys: None, + candidates: vec![], + asserted_wrapper_proof: None, + }); + + let was_frozen = operator_state.frozen_verifier_pubkeys.is_some(); + if let Some(existing) = operator_state + .candidates + .iter() + .find(|candidate| candidate.verifier_pubkey == *verifier_pubkey) + { + if existing.setup_package != *setup_package { + bail!("conflicting GenCircuits setup package for verifier {verifier_pubkey}"); } - }; - let pegin_deposit_txid = instance_params.build_pegin_tx()?.0.tx().compute_txid(); - if !tx_on_chain(ctx.btc_client, &pegin_deposit_txid).await? { - tracing::warn!( - "Ignore ConfirmInstance for {instance_id}: pegin deposit tx {pegin_deposit_txid} not found on chain" + } else if was_frozen { + tracing::debug!( + "Ignore GenCircuits for {instance_id}:{graph_id}: verifier membership is frozen" ); + return Ok(()); + } else { + operator_state.candidates.push(OperatorVerifierCandidate { + verifier_pubkey: *verifier_pubkey, + setup_package: setup_package.clone(), + verifier_index: None, + selected_circuit_indexes: vec![], + gc_data: None, + prover_state: None, + }); } - // 2. save the instance data to local db - store_instance_parameters(ctx.local_db, &instance_params).await?; + + if operator_state.frozen_verifier_pubkeys.is_none() + && operator_state.candidates.len() < todo_funcs::min_required_verifier() + { + save_babe_setup_state(ctx.local_db, instance_id, graph_id, &state)?; + + return Ok(()); + } + + freeze_operator_candidates(operator_state)?; + let cut_candidates = if was_frozen { + operator_state + .candidates + .iter() + .filter(|candidate| candidate.verifier_pubkey == *verifier_pubkey) + .cloned() + .collect::>() + } else { + operator_state.candidates.clone() + }; + + save_babe_setup_state(ctx.local_db, instance_id, graph_id, &state)?; + for candidate in cut_candidates { + let message_content = GOATMessageContent::CutCircuits(CutCircuits { + instance_id, + graph_id, + verifier_pubkey: candidate.verifier_pubkey, + verifier_index: candidate.verifier_index.unwrap(), + selected_circuit_indexes: candidate.selected_circuit_indexes, + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Verifier, message_content)).await?; + } + Ok(()) } +// generate proofs for the chosen GC and broadcast SolderingProof. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_create_graph_committee( +async fn handle_cut_circuits_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - graph: &SimplifiedBitvm2Graph, + verifier_pubkey: &PublicKey, + verifier_index: usize, + selected_circuit_indexes: &Vec, ) -> Result<()> { - // received from Operator - // 1. check graph data & operator stake - if let Err(e) = - todo_funcs::validate_init_graph(ctx.local_db, ctx.btc_client, ctx.goat_client, graph).await - { - if should_ignore_invalid_graph(&e, instance_id, graph_id, "CreateGraph", None) { - return Ok(()); - } - bail!(e) - }; - // 2. save the graph data to local db - store_graph(ctx.local_db, graph).await?; - // 3. generate Musig2 nonces & broadcast NonceGeneration - let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); - let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; - let (pub_nonces, _, nonce_sigs) = committee_master_key.nonces_for_graph_with_keypair( + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let local_verifier_pubkey: PublicKey = verifier_master_key.master_keypair().public_key().into(); + if &local_verifier_pubkey != verifier_pubkey { + tracing::debug!( + "Ignore CutCircuits for {instance_id}:{graph_id}: target verifier pubkey does not match local verifier" + ); + + return Ok(()); + } + + let Some(mut verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .and_then(|state| state.verifier) + else { + tracing::warn!( + "Ignore CutCircuits for {instance_id}:{graph_id}: missing BABE verifier setup state" + ); + return Ok(()); + }; + + if verifier_state.verifier_pubkey != *verifier_pubkey { + tracing::warn!( + "Ignore CutCircuits for {instance_id}:{graph_id}: verifier pubkey does not match saved BABE setup state" + ); + + return Ok(()); + } + + if let Some(saved_index) = verifier_state.verifier_index { + if saved_index != verifier_index { + bail!( + "CutCircuits verifier index {verifier_index} conflicts with persisted slot {saved_index}" + ); + } + if verifier_state.finalized_indices != *selected_circuit_indexes { + bail!("CutCircuits finalized indices conflict with persisted selection"); + } + if let Some(soldering) = verifier_state.soldering.clone() { + let setup_state = verifier_state; + send_soldering_proof_to_operator( + ctx.swarm, + instance_id, + graph_id, + verifier_index, + &setup_state.opened, + &setup_state.finalized, + &soldering, + ) + .await?; + + return Ok(()); + } + } + + let setup_package = verifier_state.setup_package.clone(); + get_babe_gc_asset_paths()?; + + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE opening")?; + let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let private_state = verifier_state.private_state.clone(); + let selected_indices = selected_circuit_indexes.clone(); + let package_for_opening = setup_package.clone(); + let soldering_builder = Arc::clone( + ctx.soldering_builder + .as_ref() + .context("BABE soldering builder is not initialized for Verifier")?, + ); + let (opened, finalized, soldering) = tokio::task::spawn_blocking(move || { + open_real_setup_and_solder( + &soldering_builder, + &private_state, + &package_for_opening, + &selected_indices, + &vk, + &public_inputs, + ) + }) + .await + .context("real BABE opening task failed")??; + + verifier_state.verifier_index = Some(verifier_index); + verifier_state.finalized_indices = selected_circuit_indexes.clone(); + verifier_state.opened = opened.clone(); + verifier_state.finalized = finalized.clone(); + verifier_state.soldering = Some(soldering.clone()); + + update_babe_setup_state(ctx.local_db, instance_id, graph_id, |state| { + state.verifier = Some(verifier_state); + })?; + + send_soldering_proof_to_operator( + ctx.swarm, + instance_id, + graph_id, + verifier_index, + &opened, + &finalized, + &soldering, + ) + .await?; + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +async fn handle_soldering_proof_ready_operator( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + verifier_index: usize, + payload_hash: [u8; 32], + total_len: usize, +) -> Result<()> { + if total_len == 0 { + bail!("SolderingProofReady total_len must be greater than zero"); + } + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let local_operator_pubkey = operator_master_key.master_keypair().public_key().into(); + if !pending_graph_belongs_to_operator( + ctx.local_db, + instance_id, + graph_id, + &local_operator_pubkey, + ) + .await? + { + tracing::debug!( + "Ignore SolderingProofReady for {instance_id}:{graph_id}: no local pending graph session" + ); + + return Ok(()); + } + + let state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .ok_or_else(|| anyhow!("missing BABE setup state for pending graph {graph_id}"))?; + let operator_state = state + .operator + .as_ref() + .ok_or_else(|| anyhow!("missing operator BABE setup state for pending graph {graph_id}"))?; + let frozen = operator_state + .frozen_verifier_pubkeys + .as_ref() + .ok_or_else(|| anyhow!("operator verifier membership is not frozen"))?; + let verifier_pubkey = frozen.get(verifier_index).ok_or_else(|| { + anyhow!("SolderingProofReady verifier index {verifier_index} out of range") + })?; + let candidate = operator_state + .candidates + .iter() + .find(|candidate| candidate.verifier_pubkey == *verifier_pubkey) + .ok_or_else(|| anyhow!("selected verifier candidate is missing"))?; + if candidate.verifier_index != Some(verifier_index) { + bail!("selected verifier candidate index does not match SolderingProofReady slot"); + } + + let store_base_path = get_soldering_proof_payload_store_path()?; + let payload_path = soldering_proof_payload_store_path( + &store_base_path, + instance_id, + graph_id, + verifier_index, + &payload_hash, + )?; + tracing::info!( + from_peer_id = %ctx.from_peer_id, + instance_id = %instance_id, + graph_id = %graph_id, + verifier_index, + total_len, + payload_hash = %soldering_payload_hash_hex(&payload_hash), + payload_path = %payload_path, + "received SolderingProofReady; reading payload from store" + ); + let payload = match read_soldering_proof_store_payload(&payload_path).await { + Ok(payload) => payload, + Err(err) => { + tracing::error!( + instance_id = %instance_id, + graph_id = %graph_id, + verifier_index, + payload_path = %payload_path, + error = %err, + "failed to read soldering proof payload from store" + ); + return Err(err).context("read soldering proof payload from store"); + } + }; + tracing::info!( + instance_id = %instance_id, + graph_id = %graph_id, + verifier_index, + bytes = payload.len(), + total_len, + payload_hash = %soldering_payload_hash_hex(&payload_hash), + payload_path = %payload_path, + "read soldering proof payload from store" + ); + tracing::info!( + instance_id = %instance_id, + graph_id = %graph_id, + verifier_index, + total_len, + payload_hash = %soldering_payload_hash_hex(&payload_hash), + "start processing soldering proof payload" + ); + handle_soldering_proof_payload_operator( + ctx, + instance_id, + graph_id, + verifier_index, + payload_hash, + total_len, + &payload, + ) + .await +} + +#[allow(clippy::too_many_arguments)] +pub(crate) async fn handle_soldering_proof_payload_operator( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + verifier_index: usize, + payload_hash: [u8; 32], + total_len: usize, + payload: &[u8], +) -> Result<()> { + if payload.len() != total_len { + tracing::warn!( + instance_id = %instance_id, + graph_id = %graph_id, + verifier_index, + actual_len = payload.len(), + total_len, + payload_hash = %soldering_payload_hash_hex(&payload_hash), + "SolderingProof payload length mismatch" + ); + bail!( + "SolderingProof payload length {} does not match total_len {total_len}", + payload.len() + ); + } + let actual_hash = soldering_payload_hash(payload); + if actual_hash != payload_hash { + tracing::warn!( + instance_id = %instance_id, + graph_id = %graph_id, + verifier_index, + expected_hash = %soldering_payload_hash_hex(&payload_hash), + actual_hash = %soldering_payload_hash_hex(&actual_hash), + "SolderingProof payload hash mismatch" + ); + bail!("SolderingProof payload hash mismatch"); + } + let payload: CompactSolderingProofPayload = + bincode::deserialize(payload).context("deserialize compact soldering proof payload")?; + handle_compact_soldering_proof_operator(ctx, instance_id, graph_id, verifier_index, payload) + .await +} + +// verify Verifier SolderingProof, build Graph and broadcast CreateGraph. +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_compact_soldering_proof_operator( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + verifier_index: usize, + payload: CompactSolderingProofPayload, +) -> Result<()> { + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let local_operator_pubkey = operator_master_key.master_keypair().public_key().into(); + if !pending_graph_belongs_to_operator( + ctx.local_db, + instance_id, + graph_id, + &local_operator_pubkey, + ) + .await? + { + tracing::debug!( + "Ignore SolderingProof for {instance_id}:{graph_id}: no local pending graph session" + ); + + return Ok(()); + } + + let mut state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .ok_or_else(|| anyhow!("missing BABE setup state for pending graph {graph_id}"))?; + let operator_state = state + .operator + .as_mut() + .ok_or_else(|| anyhow!("missing operator BABE setup state for pending graph {graph_id}"))?; + let frozen = operator_state + .frozen_verifier_pubkeys + .as_ref() + .ok_or_else(|| anyhow!("operator verifier membership is not frozen"))?; + let verifier_pubkey = *frozen + .get(verifier_index) + .ok_or_else(|| anyhow!("SolderingProof verifier index {verifier_index} out of range"))?; + + let candidate = operator_state + .candidates + .iter() + .find(|candidate| candidate.verifier_pubkey == verifier_pubkey) + .ok_or_else(|| anyhow!("selected verifier candidate is missing"))?; + if candidate.verifier_index != Some(verifier_index) { + bail!("selected verifier candidate index does not match SolderingProof slot"); + } + let setup_package = candidate.setup_package.clone(); + let (opened, finalized, soldering) = + expand_compact_soldering_proof_payload(&setup_package, payload) + .context("expand compact soldering proof payload")?; + + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE validation")?; + let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let package_for_validation = setup_package.clone(); + let opened_for_validation = opened.clone(); + let finalized_for_validation = finalized.clone(); + let soldering_for_validation = soldering.clone(); + let soldering_builder = Arc::clone( + ctx.soldering_builder + .as_ref() + .context("BABE soldering builder is not initialized for Operator")?, + ); + + tokio::task::spawn_blocking(move || { + verify_real_setup( + &soldering_builder, + &package_for_validation, + &opened_for_validation, + &finalized_for_validation, + &soldering_for_validation, + &vk, + &public_inputs, + ) + }) + .await + .context("real BABE setup verification task failed")??; + + let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey)?; + let prover_state = BabeProverState { + package: setup_package.clone(), + finalized, + soldering, + h_msgs: gc_data.final_msg_hashlocks.clone(), + }; + let Some(bitvm_gc_circuit_datas) = record_candidate_gc_data( + operator_state, + verifier_pubkey, + verifier_index, + &setup_package, + gc_data, + prover_state, + )? + else { + save_babe_setup_state(ctx.local_db, instance_id, graph_id, &state)?; + return Ok(()); + }; + save_babe_setup_state(ctx.local_db, instance_id, graph_id, &state)?; + + let instance_params = get_instance_parameters(ctx.local_db, instance_id) + .await? + .ok_or_else(|| anyhow!("Instance parameters not found for {instance_id}"))?; + + let (graph_nonce, cur_prekickoff_txn) = + match get_current_prekickoff_tx(ctx.local_db, &local_operator_pubkey).await? { + Some((graph_nonce, prekickoff_tx)) => (graph_nonce, prekickoff_tx), + None => (0, build_genesis_prekickoff_tx(ctx.btc_client, ctx.goat_client).await?), + }; + let prekickoff_params = + build_prekickoff_params(ctx.btc_client, graph_nonce, cur_prekickoff_txn).await?; + + let graph_params = build_graph_params( + ctx.local_db, + ctx.goat_client, + instance_params, + prekickoff_params, + bitvm_gc_circuit_datas, + graph_nonce, + graph_id, + ) + .await?; + + let mut graph = generate_bitvm_graph(graph_params)?; + operator_pre_sign(operator_master_key.master_keypair(), &mut graph)?; + + let graph = graph.to_simplified()?; + store_graph(ctx.local_db, &graph).await?; + + let mut storage = ctx.local_db.acquire().await?; + storage.delete_pending_graph_init(&instance_id, &local_operator_pubkey.to_string()).await?; + + let message_content = + GOATMessageContent::CreateGraph(CreateGraph { instance_id, graph_id, graph_nonce, graph }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; + + Ok(()) +} + +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id))] +async fn handle_confirm_instance_default( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, +) -> Result<()> { + // triggered by PeginDeposit tx + // 1. read & check parameters + let instance_params = match read_instance_info_from_goat(ctx.goat_client, instance_id).await { + Ok(v) => v, + Err(e) => { + if should_ignore_invalid_pegin_data(&e, instance_id) { + return Ok(()); + } + bail!(e) + } + }; + let pegin_deposit_txid = instance_params.build_pegin_tx()?.0.tx().compute_txid(); + if !tx_on_chain(ctx.btc_client, &pegin_deposit_txid).await? { + tracing::warn!( + "Ignore ConfirmInstance for {instance_id}: pegin deposit tx {pegin_deposit_txid} not found on chain" + ); + return Ok(()); + } + // 2. save the instance data to local db + store_instance_parameters(ctx.local_db, &instance_params).await?; + Ok(()) +} + +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_create_graph_committee( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + graph: &SimplifiedBitvmGcGraph, +) -> Result<()> { + // received from Operator + // 1. check graph data & operator stake + if let Err(e) = + todo_funcs::validate_init_graph(ctx.local_db, ctx.btc_client, ctx.goat_client, graph).await + { + if should_ignore_invalid_graph(&e, instance_id, graph_id, "CreateGraph", None) { + return Ok(()); + } + bail!(e) + }; + // 2. save the graph data to local db + store_graph(ctx.local_db, graph).await?; + // 3. generate Musig2 nonces & broadcast NonceGeneration + let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); + let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; + let verifier_num = graph.parameters.gc_data.len(); + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + let (pub_nonces, _, nonce_sigs) = committee_master_key.nonces_for_graph_with_keypair( instance_id, graph_id, - graph.parameters.watchtower_pubkeys.len(), - graph.assert_commit_num, + watchtower_num, + verifier_num, instance_keypair, ); let local_committee_pubkey = instance_keypair.public_key().into(); @@ -975,8 +1812,6 @@ async fn handle_create_graph_committee( instance_id, graph_id, committee_pubkey: local_committee_pubkey, - watchtower_num: graph.parameters.watchtower_pubkeys.len(), - assert_commit_num: graph.assert_commit_num, pub_nonces: pub_nonces.clone(), nonce_sigs, }); @@ -994,12 +1829,12 @@ async fn handle_create_graph_committee( let pub_nonces_unchecked = get_committee_pub_nonces_for_graph(ctx.local_db, instance_id, graph_id).await?; if pub_nonces_unchecked.len() == committee_pubkeys.len() { - let mut graph = Bitvm2Graph::from_simplified(graph)?; + let mut graph = BitvmGcGraph::from_simplified(graph)?; + let verifier_num = graph.verifier_asserts.len(); let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let assert_commit_num = graph.assert_commit_timeout_txns.len(); let mut pub_nonces = Vec::with_capacity(pub_nonces_unchecked.len()); for (pk, pn) in pub_nonces_unchecked.into_iter() { - if let Err(e) = pn.validate_length(watchtower_num, assert_commit_num) { + if let Err(e) = pn.validate_length(watchtower_num, verifier_num) { tracing::warn!("PubNonces from {} has invalid length: {e}", pk.to_string()); return Ok(()); } @@ -1012,7 +1847,7 @@ async fn handle_create_graph_committee( instance_id, graph_id, watchtower_num, - assert_commit_num, + verifier_num, instance_keypair, ); let committee_partial_sigs = @@ -1029,15 +1864,12 @@ async fn handle_create_graph_committee( Ok(()) } -#[allow(clippy::too_many_arguments)] #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_nonce_generation_committee( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, received_committee_pubkey: &PublicKey, - watchtower_num: usize, - assert_commit_num: usize, pub_nonces: &CommitteePubNonces, nonce_sigs: &CommitteeNonceSignatures, content: &GOATMessageContent, @@ -1055,13 +1887,29 @@ async fn handle_nonce_generation_committee( return Ok(()); } // 1. check pub_nonces & nonce signatures + let message = make_message(ctx, content); + let graph = match get_graph_or_defer( + ctx.swarm, + ctx.local_db, + ctx.goat_client, + instance_id, + graph_id, + &message, + ) + .await? + { + Some(g) => g, + None => return Ok(()), + }; + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + let verifier_num = graph.parameters.gc_data.len(); let committee_xonly_pubkey = XOnlyPublicKey::from(*received_committee_pubkey); if !verify_nonce_signatures( &committee_xonly_pubkey, pub_nonces, nonce_sigs, watchtower_num, - assert_commit_num, + verifier_num, )? { tracing::warn!( "Ignore NonceGeneration for {instance_id}:{graph_id} from {}: invalid pub_nonces or nonce_sigs", @@ -1087,26 +1935,12 @@ async fn handle_nonce_generation_committee( let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; let local_committee_pubkey = instance_keypair.public_key().into(); - let message = make_message(ctx, content); - let graph = match get_graph_or_defer( - ctx.swarm, - ctx.local_db, - ctx.goat_client, - instance_id, - graph_id, - &message, - ) - .await? - { - Some(g) => g, - None => return Ok(()), - }; - let mut graph = Bitvm2Graph::from_simplified(&graph)?; + let mut graph = BitvmGcGraph::from_simplified(&graph)?; + let verifier_num = graph.verifier_asserts.len(); let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let assert_commit_num = graph.assert_commit_timeout_txns.len(); let mut pub_nonces = Vec::with_capacity(pub_nonces_unchecked.len()); for (pk, pn) in pub_nonces_unchecked.into_iter() { - if let Err(e) = pn.validate_length(watchtower_num, assert_commit_num) { + if let Err(e) = pn.validate_length(watchtower_num, verifier_num) { tracing::warn!("PubNonces from {} has invalid length: {e}", pk.to_string()); return Ok(()); } @@ -1117,7 +1951,7 @@ async fn handle_nonce_generation_committee( instance_id, graph_id, watchtower_num, - assert_commit_num, + verifier_num, instance_keypair, ); // 4. if received enough valid committee partial sigs, endorse the graph @@ -1162,15 +1996,12 @@ async fn handle_nonce_generation_committee( Ok(()) } -#[allow(clippy::too_many_arguments)] #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_nonce_generation_operator( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, received_committee_pubkey: &PublicKey, - watchtower_num: usize, - assert_commit_num: usize, pub_nonces: &CommitteePubNonces, nonce_sigs: &CommitteeNonceSignatures, ) -> Result<()> { @@ -1186,6 +2017,18 @@ async fn handle_nonce_generation_operator( { return Ok(()); } + let graph = match get_graph(ctx.local_db, instance_id, graph_id).await? { + Some(g) => g, + None => { + tracing::warn!( + "Ignore NonceGeneration for {instance_id}:{graph_id} from {}: graph not found, maybe belongs to another Operator", + received_committee_pubkey.to_string() + ); + return Ok(()); + } + }; + let verifier_num = graph.parameters.gc_data.len(); + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); // 1. check pub_nonces & nonce signatures let committee_xonly_pubkey = XOnlyPublicKey::from(*received_committee_pubkey); if !verify_nonce_signatures( @@ -1193,7 +2036,7 @@ async fn handle_nonce_generation_operator( pub_nonces, nonce_sigs, watchtower_num, - assert_commit_num, + verifier_num, )? { tracing::warn!( "Ignore NonceGeneration for {instance_id}:{graph_id} from {}: invalid pub_nonces or nonce_sigs", @@ -1201,19 +2044,7 @@ async fn handle_nonce_generation_operator( ); return Ok(()); } - let graph = match get_graph(ctx.local_db, instance_id, graph_id).await? { - Some(g) => g, - None => { - tracing::warn!( - "Ignore NonceGeneration for {instance_id}:{graph_id} from {}: graph not found, maybe belongs to another Operator", - received_committee_pubkey.to_string() - ); - return Ok(()); - } - }; - let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let assert_commit_num = graph.assert_commit_num; - if let Err(e) = pub_nonces.validate_length(watchtower_num, assert_commit_num) { + if let Err(e) = pub_nonces.validate_length(watchtower_num, verifier_num) { tracing::warn!( "Ignore NonceGeneration for {instance_id}:{graph_id} from {}: invalid pub_nonces length: {e}", received_committee_pubkey.to_string() @@ -1301,7 +2132,7 @@ async fn handle_committee_presign_committee( Some(g) => g, None => return Ok(()), }; - let graph = Bitvm2Graph::from_simplified(&graph)?; + let graph = BitvmGcGraph::from_simplified(&graph)?; let committee_sig_for_graph = endorse_graph(ctx.goat_client, &graph).await?; let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; @@ -1392,7 +2223,7 @@ async fn handle_endorse_graph_operator( return Ok(()); } }; - let full_graph = Bitvm2Graph::from_simplified(&graph)?; + let full_graph = BitvmGcGraph::from_simplified(&graph)?; if let Err(e) = verify_graph_endorsement( ctx.goat_client, committee_evm_address, @@ -1438,7 +2269,7 @@ async fn handle_graph_finalize_committee( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - graph: &SimplifiedBitvm2Graph, + graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], ) -> Result<()> { // received from Operator @@ -1512,7 +2343,7 @@ async fn handle_graph_finalize_committee( // already posted return Ok(()); } - let graph = Bitvm2Graph::from_simplified(graph)?; + let graph = BitvmGcGraph::from_simplified(graph)?; let graph_data = build_graph_data(&graph)?; let endorse_sigs = endorse_sigs.iter().map(|(_, _, sig)| sig.clone()).collect::>(); ctx.goat_client @@ -1527,7 +2358,7 @@ async fn handle_graph_finalize_default( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - graph: &SimplifiedBitvm2Graph, + graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], ) -> Result<()> { // received from Operator @@ -1602,7 +2433,7 @@ async fn handle_pegin_confirm_nonce_committee( .await? .ok_or_else(|| anyhow!("Instance parameters not found for {instance_id}"))?; let mut pegin_confirm = instance_params.build_pegin_tx()?.1; - let context = instance_params.get_verifier_context(instance_keypair)?; + let context = instance_params.get_committee_context(instance_keypair)?; let partial_sig = pegin_confirm .sign_input_0_musig2(&context, &sec_nonce, &agg_nonce) .map_err(|e| anyhow!("Failed to sign pegin confirm for {instance_id}: {e}"))?; @@ -1835,7 +2666,7 @@ async fn handle_post_ready(ctx: &mut HandlerContext<'_>, instance_id: Uuid) -> R let graph = get_graph(ctx.local_db, instance_id, *graph_id) .await? .ok_or_else(|| anyhow!("Graph not found for {instance_id}:{graph_id}"))?; - let graph = Bitvm2Graph::from_simplified(&graph)?; + let graph = BitvmGcGraph::from_simplified(&graph)?; let graph_data = build_graph_data(&graph)?; ctx.goat_client .gateway_post_graph_data(&instance_id, graph_id, &graph_data, &endorsement_sigs) @@ -1866,7 +2697,7 @@ async fn handle_kickoff_ready_operator( Some(g) => g, None => return Ok(()), }; - let mut graph = Bitvm2Graph::from_simplified(&graph)?; + let mut graph = BitvmGcGraph::from_simplified(&graph)?; let operator_pubkey = graph.parameters.operator_pubkey; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let node_pubkey: PublicKey = operator_master_key.master_keypair().public_key().into(); @@ -1917,14 +2748,14 @@ async fn handle_kickoff_ready_operator( Some(g) => g, None => return Ok(()), }; - let mut current_graph = Bitvm2Graph::from_simplified(¤t_graph)?; + let mut current_graph = BitvmGcGraph::from_simplified(¤t_graph)?; let current_graph_start_status = get_graph_status(ctx.local_db, current_instance_id, current_graph_id) .await? .ok_or_else(|| { anyhow!("Graph status not found for {current_instance_id}:{current_graph_id}") })?; - let (current_graph_status, current_graph_sub_status) = refresh_graph( + let (current_graph_status, _current_graph_sub_status, current_graph_scan) = refresh_graph( ctx.local_db, ctx.btc_client, ctx.goat_client, @@ -1941,10 +2772,10 @@ async fn handle_kickoff_ready_operator( current_instance_id, current_graph_id, Some(¤t_graph), + current_graph_scan.as_ref(), Some(current_graph_start_status), current_graph_start_status, current_graph_status, - current_graph_sub_status, ) .await?; if current_graph_status.is_closed() { @@ -2089,7 +2920,7 @@ async fn handle_kickoff_sent_committee( } #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_kickoff_sent_challenger( +async fn handle_kickoff_sent_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, @@ -2187,7 +3018,7 @@ async fn handle_kickoff_sent_default( } #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_prekickoff_sent_challenger( +async fn handle_prekickoff_sent_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, @@ -2240,7 +3071,7 @@ async fn handle_prekickoff_sent_challenger( Some(g) => g, None => return Ok(()), }; - let prev_graph = Bitvm2Graph::from_simplified(&prev_graph)?; + let prev_graph = BitvmGcGraph::from_simplified(&prev_graph)?; let prev_graph_start_status = get_graph_status(ctx.local_db, prev_instance_id, prev_graph_id) .await? .ok_or_else(|| anyhow!("Graph status not found for {prev_instance_id}:{prev_graph_id}"))?; @@ -2255,10 +3086,10 @@ async fn handle_prekickoff_sent_challenger( .await?; if !tx_on_chain(ctx.btc_client, &prev_graph.kickoff.tx().compute_txid()).await? { // 2. if previous kickoff not started, broadcast force-skip-kickoff txn - challenger_force_skip_kickoff(ctx.btc_client, &prev_graph).await?; + verifier_force_skip_kickoff(ctx.btc_client, &prev_graph).await?; } else if !prev_graph_status.is_closed() { // 3. if previous kickoff is not closed, broadcast quick-challenge/challenge-incomplete-kickoff txn - challenger_quick_challenge(ctx.btc_client, &prev_graph).await?; + verifier_quick_challenge(ctx.btc_client, &prev_graph).await?; } Ok(()) } @@ -2305,9 +3136,12 @@ async fn handle_challenge_sent_operator( ); return Ok(()); } - let kickoff_txid = graph.kickoff.tx().compute_txid(); if let Some(challenge_tx) = ctx.btc_client.get_tx(&challenge_txid).await? { - let challenge_outpoint = OutPoint { txid: kickoff_txid, vout: 0 }; + let challenge_outpoint = graph + .kickoff + .connector_a_input() + .map_err(|e| anyhow!("failed to get connector-a input: {e}"))? + .outpoint; if challenge_tx.input[0].previous_output != challenge_outpoint { tracing::warn!( "Ignore ChallengeSent for {instance_id}:{graph_id}: invalid challenge tx input" @@ -2340,6 +3174,12 @@ async fn handle_challenge_sent_operator( } None => broadcast_tx(ctx.btc_client, &watchtower_challenge_init_tx).await?, }; + let message_content = + GOATMessageContent::WatchtowerChallengeInitSent(WatchtowerChallengeInitSent { + instance_id, + graph_id, + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Watchtower, message_content)).await?; Ok(()) } @@ -2404,77 +3244,78 @@ async fn handle_watchtower_challenge_init_sent_watchtower( ); return Ok(()); } - if outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, 2 * node_index as u64) - .await? - .is_some() + let watchtower_challenge_vout = + output_topology::watchtower_challenge_init::watchtower_connector(node_index) as u64; + if outpoint_spent_txid( + ctx.btc_client, + &watchtower_challenge_init_txid, + watchtower_challenge_vout, + ) + .await? + .is_some() { tracing::warn!( "Ignore WatchtowerChallengeInitSent for {instance_id}:{graph_id}: watchtower challenge already spent" ); return Ok(()); } - // 1. check the withdraw status on GoatChain, if the withdraw is invalid, sign & broadcast watchtower-challenge txn - let withdraw_status = ctx.goat_client.gateway_get_withdraw_data(&graph_id).await?.status; - if crate::env::should_always_challenge() - || [WithdrawStatus::None, WithdrawStatus::Canceled].contains(&withdraw_status) + // watchtower should always challenge + let watchtower_proof = match get_watchtower_commitment( + ctx.local_db, + ctx.btc_client, + ctx.http_client, + instance_id, + graph_id, + ) + .await? { - let watchtower_proof = match get_watchtower_commitment( - ctx.local_db, - ctx.btc_client, - ctx.http_client, - instance_id, - graph_id, - ) - .await? - { - (Some(p), _) => p, - (None, wait_secs) => { - tracing::warn!( - "Retry WatchtowerChallengeInitSent for {instance_id}:{graph_id} later: watchtower proof not ready, retry after {wait_secs} seconds" - ); - push_local_unhandled_messages(ctx.local_db, graph_id, &message, wait_secs).await?; - return Ok(()); - } - }; - let watchtower_challenge_txid = match send_watchtower_challenge_tx( - ctx.btc_client, - &graph, - node_index, - watchtower_proof, - ) - .await - { - Ok(txid) => txid, - Err(e) => { - tracing::warn!( - "Ignore WatchtowerChallengeInitSent for {instance_id}:{graph_id}: failed to send watchtower challenge tx: {e}" - ); - return Ok(()); - } - }; - let message_content = - GOATMessageContent::WatchtowerChallengeSent(WatchtowerChallengeSent { - instance_id, - graph_id, - watchtower_challenge_txids: vec![(node_index, watchtower_challenge_txid)], - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; - } + (Some(p), _) => p, + (None, wait_secs) => { + tracing::warn!( + "Retry WatchtowerChallengeInitSent for {instance_id}:{graph_id} later: watchtower proof not ready, retry after {wait_secs} seconds" + ); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, wait_secs).await?; + return Ok(()); + } + }; + let watchtower_challenge_txid = match send_watchtower_challenge_tx( + ctx.btc_client, + &graph, + node_index, + watchtower_proof, + ) + .await + { + Ok(txid) => txid, + Err(e) => { + tracing::warn!( + "Ignore WatchtowerChallengeInitSent for {instance_id}:{graph_id}: failed to send watchtower challenge tx: {e}" + ); + return Ok(()); + } + }; + tracing::info!( + "WatchtowerChallengeSent for {instance_id}:{graph_id}: watchtower_index={node_index}, txid={watchtower_challenge_txid}" + ); + let message_content = GOATMessageContent::WatchtowerChallengeSent(WatchtowerChallengeSent { + instance_id, + graph_id, + watchtower_index: node_index, + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Operator, message_content)).await?; Ok(()) } -#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id, watchtower_index = watchtower_index))] async fn handle_watchtower_challenge_sent_operator( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - watchtower_challenge_txids: &Vec<(usize, Txid)>, + watchtower_index: usize, content: &GOATMessageContent, ) -> Result<()> { - // triggered by WatchtowerChallenge tx let message = make_message(ctx, content); - // 1. check the watchtower-challenge tx status on Bitcoin chain, if watchtower challenge tx is confirmed, sign & broadcast operator-ack txn - let (mut graph, graph_status, _graph_sub_status) = match refresh_graph_status( + let (graph, graph_status, _graph_sub_status) = match refresh_graph_status( ctx, instance_id, graph_id, @@ -2492,46 +3333,66 @@ async fn handle_watchtower_challenge_sent_operator( ); return Ok(()); } + let watchtower_num = validate_watchtower_branches(&graph)?; + if watchtower_index >= watchtower_num { + bail!("watchtower index {watchtower_index} out of range for {watchtower_num} slots"); + } + let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); - let operator_graph_keypair = operator_master_key.master_keypair(); - let operator_master_keypair = operator_master_key.master_keypair(); - for (watchtower_index, watchtower_challenge_txid) in watchtower_challenge_txids { - tracing::info!( - "Handle WatchtowerChallengeSent for {instance_id}:{graph_id}, watchtower index: {watchtower_index}, watchtower challenge txid: {watchtower_challenge_txid}" + let watchtower_vout = + output_topology::watchtower_challenge_init::watchtower_connector(watchtower_index) as u64; + let Some((watchtower_spent_txid, watchtower_vin, _txin)) = + outpoint_spent_txin(ctx.btc_client, &watchtower_challenge_init_txid, watchtower_vout) + .await? + else { + tracing::warn!( + "Ignore WatchtowerChallengeSent for {instance_id}:{graph_id}: watchtower connector {watchtower_index} is not spent yet" ); - let watchtower_challenge_tx = match ctx.btc_client.get_tx(watchtower_challenge_txid).await? - { - Some(tx) => tx, - None => { - tracing::warn!( - "Ignore WatchtowerChallengeSent for {instance_id}:{graph_id}: watchtower challenge tx not found on chain: {watchtower_challenge_txid}" - ); - continue; - } - }; - let watchtower_challenge_outpoint = - OutPoint { txid: watchtower_challenge_init_txid, vout: 2 * *watchtower_index as u32 }; - if watchtower_challenge_tx.input[0].previous_output != watchtower_challenge_outpoint { - tracing::warn!( - "Ignore WatchtowerChallengeSent for {instance_id}:{graph_id}: invalid watchtower challenge tx input" - ); - continue; - } - let preimage = - todo_funcs::get_preimage(ctx.local_db, instance_id, graph_id, *watchtower_index) - .await?; - let (ack_txin, ack_txin_amount) = - operator_sign_ack(operator_graph_keypair, &mut graph, *watchtower_index, &preimage)?; - build_sign_and_broadcast_tx( - ctx.btc_client, - operator_master_keypair, - vec![ack_txin], - ack_txin_amount, - vec![], - ) - .await?; + return Ok(()); + }; + if watchtower_vin != 0 { + tracing::warn!( + "Ignore WatchtowerChallengeSent for {instance_id}:{graph_id}: watchtower connector {watchtower_index} was spent by tx {watchtower_spent_txid} at unexpected input index {watchtower_vin}" + ); + return Ok(()); + } + let timeout_txid = graph.watchtower_challenge_timeouts[watchtower_index].tx().compute_txid(); + if watchtower_spent_txid == timeout_txid { + tracing::warn!( + "Ignore WatchtowerChallengeSent for {instance_id}:{graph_id}: watchtower connector {watchtower_index} was spent by timeout tx {timeout_txid}" + ); + return Ok(()); + } + + let ack_vout = + output_topology::watchtower_challenge_init::ack_connector(watchtower_index) as u64; + if let Some(spent_txid) = + outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, ack_vout).await? + { + tracing::warn!( + "Ignore WatchtowerChallengeSent for {instance_id}:{graph_id}: ack connector {watchtower_index} already spent by {spent_txid}" + ); + return Ok(()); } + + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let preimage = operator_master_key.preimage_for_graph(graph_id, watchtower_index); + let ack_connector_input = graph + .watchtower_challenge_init + .ack_connector_input(watchtower_index) + .map_err(|e| anyhow!("failed to get ack connector input: {e}"))?; + let ack_input = operator_sign_challenge_ack(&graph, watchtower_index, &preimage)?; + let ack_txid = build_sign_and_broadcast_tx( + ctx.btc_client, + operator_master_key.master_keypair(), + vec![ack_input], + ack_connector_input.amount, + vec![goat::scripts::p2a_output()], + ) + .await?; + tracing::info!( + "Operator challenge ACK sent for {instance_id}:{graph_id}: watchtower_index={watchtower_index}, txid={ack_txid}" + ); Ok(()) } @@ -2540,10 +3401,8 @@ async fn handle_watchtower_challenge_timeout_operator( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - watchtower_indexes: &Vec, content: &GOATMessageContent, ) -> Result<()> { - // triggered by timeout task let message = make_message(ctx, content); let (mut graph, graph_status, _graph_sub_status) = match refresh_graph_status( ctx, @@ -2563,91 +3422,55 @@ async fn handle_watchtower_challenge_timeout_operator( ); return Ok(()); } + + let watchtower_num = validate_watchtower_branches(&graph)?; let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let watchtower_challenge_init_height = match ctx - .btc_client - .get_tx_status(&watchtower_challenge_init_txid) - .await? - .block_height - { - Some(height) => height, - None => { - tracing::warn!( - "Ignore WatchtowerChallengeTimeout for {instance_id}:{graph_id}: watchtower challenge init tx not confirmed yet" - ); - return Ok(()); - } - }; - let current_height = ctx.btc_client.get_height().await?; - if current_height - < watchtower_challenge_init_height + watchtower_challenge_timeout_timelock(get_network()) - { - tracing::warn!( - "Ignore WatchtowerChallengeTimeout for {instance_id}:{graph_id}: watchtower challenge timelock not expired yet" - ); - return Ok(()); - } let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); - let operator_master_keypair = operator_master_key.master_keypair(); - // 1. sign & broadcast watchtower-challenge-timeout txn - for watchtower_index in watchtower_indexes { - tracing::info!( - "Handle WatchtowerChallengeTimeout for {instance_id}:{graph_id}, watchtower index: {watchtower_index}" - ); - let watchtower_challenge_vout = 2 * *watchtower_index as u64; - if outpoint_spent_txid( - ctx.btc_client, - &watchtower_challenge_init_txid, - watchtower_challenge_vout, - ) - .await? - .is_some() + for watchtower_index in 0..watchtower_num { + let watchtower_vout = + output_topology::watchtower_challenge_init::watchtower_connector(watchtower_index) + as u64; + let ack_vout = + output_topology::watchtower_challenge_init::ack_connector(watchtower_index) as u64; + if outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, watchtower_vout) + .await? + .is_some() + || outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, ack_vout) + .await? + .is_some() { - tracing::warn!( - "Ignore WatchtowerChallengeTimeout for {instance_id}:{graph_id}: watchtower challenge already spent" - ); continue; } - let watchtower_challenge_timeout_tx = operator_sign_watchtower_challenge_timeout( - operator_master_keypair, + + let timeout_tx = operator_sign_watchtower_challenge_timeout( + operator_master_key.master_keypair(), &mut graph, - *watchtower_index, + watchtower_index, )?; - let anchor_vout = watchtower_challenge_timeout_tx.output.len() as u64 - 1; - let watchtower_challenge_timeout_tx_total_input_amount = graph - .watchtower_challenge_timeout_txns - .get(*watchtower_index) - .ok_or_else(|| anyhow!("WatchtowerChallengeTimeout txn not found for {instance_id}:{graph_id}:{watchtower_index}"))? + if tx_on_chain(ctx.btc_client, &timeout_tx.compute_txid()).await? { + continue; + } + let timeout_txid = timeout_tx.compute_txid(); + let timeout_tx_total_input_amount = graph.watchtower_challenge_timeouts[watchtower_index] .prev_outs() .iter() .map(|o| o.value) - .sum(); - let child_tx = build_cpfp_txns( - ctx.btc_client, - &watchtower_challenge_timeout_tx, - anchor_vout, - watchtower_challenge_timeout_tx_total_input_amount, - ) - .await?; - match child_tx { - Some(tx) => { - broadcast_package(ctx.btc_client, &[watchtower_challenge_timeout_tx, tx], true) - .await? - } - None => broadcast_tx(ctx.btc_client, &watchtower_challenge_timeout_tx).await?, - }; + .sum::(); + broadcast_tx_with_cpfp(ctx.btc_client, timeout_tx, timeout_tx_total_input_amount).await?; + tracing::info!( + "Watchtower challenge timeout sent for {instance_id}:{graph_id}: watchtower_index={watchtower_index}, txid={timeout_txid}" + ); } Ok(()) } #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_operator_ack_timeout_challenger( +async fn handle_nack_ready_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, content: &GOATMessageContent, ) -> Result<()> { - // triggered by timeout task let message = make_message(ctx, content); let (graph, graph_status, _graph_sub_status) = match refresh_graph_status( ctx, @@ -2663,162 +3486,101 @@ async fn handle_operator_ack_timeout_challenger( }; if graph_status != GraphStatus::Challenge { tracing::warn!( - "Ignore AckTimeout for {instance_id}:{graph_id}: graph status is {graph_status:?}" + "Ignore NackReady for {instance_id}:{graph_id}: graph status is {graph_status:?}" ); return Ok(()); } + let watchtower_num = validate_watchtower_branches(&graph)?; + let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let connector_f_vout = 1 + 2 * graph.parameters.watchtower_pubkeys.len() as u64; + let connector_f_vout = + output_topology::watchtower_challenge_init::connector_f(watchtower_num) as u64; if outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, connector_f_vout) .await? .is_some() { - tracing::warn!( - "Ignore OperatorAckTimeout for {instance_id}:{graph_id}: connector_F already spent" - ); + tracing::warn!("Ignore NackReady for {instance_id}:{graph_id}: connector-F already spent"); return Ok(()); } - let current_height = ctx.btc_client.get_height().await?; - let watchtower_challenge_init_height = match ctx - .btc_client - .get_tx_status(&watchtower_challenge_init_txid) - .await? - .block_height - { - Some(height) => height, - None => { - tracing::warn!( - "Ignore OperatorAckTimeout for {instance_id}:{graph_id}: watchtower challenge init tx not confirmed yet" - ); - return Ok(()); + + for watchtower_index in 0..watchtower_num { + let watchtower_vout = + output_topology::watchtower_challenge_init::watchtower_connector(watchtower_index) + as u64; + let Some(watchtower_spent_txid) = + outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, watchtower_vout) + .await? + else { + continue; + }; + + let timeout_txid = + graph.watchtower_challenge_timeouts[watchtower_index].tx().compute_txid(); + if watchtower_spent_txid == timeout_txid { + continue; } - }; - if current_height < watchtower_challenge_init_height + nack_timelock(get_network()) { - tracing::warn!( - "Ignore OperatorAckTimeout for {instance_id}:{graph_id}: nack timelock not expired yet" - ); - return Ok(()); - } - let mut nack_index = None; - for watchtower_index in 0..graph.parameters.watchtower_pubkeys.len() { - let ack_vout = 1 + 2 * watchtower_index as u64; + + let ack_vout = + output_topology::watchtower_challenge_init::ack_connector(watchtower_index) as u64; if outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, ack_vout) .await? - .is_none() + .is_some() { - nack_index = Some(watchtower_index); - break; + continue; } - } - let nack_index = match nack_index { - Some(index) => index, - None => { - tracing::warn!( - "Ignore OperatorAckTimeout for {instance_id}:{graph_id}: all ack connectors already spent" - ); + + let nack = graph + .operator_challenge_nacks + .get(watchtower_index) + .ok_or_else(|| anyhow!("invalid watchtower index {watchtower_index}"))?; + let nack_tx = nack.tx().clone(); + let nack_txid = nack_tx.compute_txid(); + if tx_on_chain(ctx.btc_client, &nack_txid).await? { return Ok(()); } - }; - // 1. broadcast Nack txn - let nack_tx = graph - .nack_txns - .get(nack_index) - .ok_or_else(|| anyhow!("Nack txn not found for {instance_id}:{graph_id}:{nack_index}"))? - .finalize(); - let anchor_vout = nack_tx.output.len() as u64 - 1; - let nack_tx_total_input_amount = - graph.nack_txns[nack_index].prev_outs().iter().map(|o| o.value).sum(); - let child_tx = - build_cpfp_txns(ctx.btc_client, &nack_tx, anchor_vout, nack_tx_total_input_amount).await?; - match child_tx { - Some(tx) => broadcast_package(ctx.btc_client, &[nack_tx, tx], true).await?, - None => broadcast_package(ctx.btc_client, &[nack_tx], true).await?, - }; + let nack_tx_total_input_amount = nack.prev_outs().iter().map(|o| o.value).sum::(); + broadcast_tx_with_cpfp(ctx.btc_client, nack_tx, nack_tx_total_input_amount).await?; + tracing::info!( + "Operator challenge NACK sent for {instance_id}:{graph_id}: watchtower_index={watchtower_index}, txid={nack_txid}" + ); + return Ok(()); + } + + tracing::warn!( + "Ignore NackReady for {instance_id}:{graph_id}: no watchtower branch is ready for NACK" + ); Ok(()) } #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_operator_commit_blockhash_ready_operator( +async fn handle_operator_commit_pubin_ready_operator( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, content: &GOATMessageContent, ) -> Result<()> { - // triggered by timeout task let message = make_message(ctx, content); - let (mut graph, graph_status, _graph_sub_status) = match refresh_graph_status( - ctx, - instance_id, - graph_id, - Some(&message), - GraphStatus::Challenge, - ) - .await? - { - Some(v) => v, - None => return Ok(()), - }; - if graph_status != GraphStatus::Challenge { - tracing::warn!( - "Ignore CommitBlockHashReady for {instance_id}:{graph_id}: graph status is {graph_status:?}" - ); + let Some((_graph, graph_status, _graph_sub_status)) = + refresh_graph_status(ctx, instance_id, graph_id, Some(&message), GraphStatus::Challenge) + .await? + else { return Ok(()); - } - // 1. check that all WatchtowerChallenge Connectors are spent - let largest_watchtower_challenge_block_hash = match get_largest_watchtower_challenge_block( - &graph, - ctx.btc_client, - ) - .await - { - Ok(d) => d, - Err(e) => { - tracing::warn!( - "Retry OperatorCommitBlockHashReady later for {instance_id}:{graph_id}: failed to get - largest watchtower challenge block, error: {e:?}" - ); - push_local_unhandled_messages( - ctx.local_db, - graph_id, - &message, - todo_funcs::avg_block_time_secs(ctx.btc_client.network()) as usize, - ) - .await?; - return Ok(()); - } }; - // 2. sign & broadcast commit-blockhash txn - let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); - let operator_graph_keypair = operator_master_key.master_keypair(); - let operator_master_keypair = operator_master_key.master_keypair(); - let wots_secret_keys = operator_master_key.wots_keypair_for_graph(graph.parameters.graph_id).0; - let blockhash_wots_secret_key = &wots_secret_keys[0]; - let (operator_commit_blockhash_txin, operator_commit_blockhash_txin_amount) = - operator_sign_blockhash_commit( - operator_graph_keypair, - &mut graph, - &largest_watchtower_challenge_block_hash.to_byte_array(), - blockhash_wots_secret_key, - )?; - build_sign_and_broadcast_tx( - ctx.btc_client, - operator_master_keypair, - vec![operator_commit_blockhash_txin], - operator_commit_blockhash_txin_amount, - vec![], - ) - .await?; + // TODO(gc-v2): build operator-commit-pubin after pubin commitment data is wired. + // It is not pre-signed, so add a funding input for fees and broadcast it normally. + tracing::warn!( + "OperatorCommitPubinReady for {instance_id}:{graph_id} is not implemented yet; graph status is {graph_status:?}" + ); Ok(()) } #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_operator_commit_blockhash_timeout_challenger( +async fn handle_operator_commit_pubin_timeout_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, content: &GOATMessageContent, ) -> Result<()> { - // triggered by timeout task let message = make_message(ctx, content); let (graph, graph_status, _graph_sub_status) = match refresh_graph_status( ctx, @@ -2834,484 +3596,434 @@ async fn handle_operator_commit_blockhash_timeout_challenger( }; if graph_status != GraphStatus::Challenge { tracing::warn!( - "Ignore CommitBlockHashTimeout for {instance_id}:{graph_id}: graph status is {graph_status:?}" + "Ignore OperatorCommitPubinTimeout for {instance_id}:{graph_id}: graph status is {graph_status:?}" ); return Ok(()); } + let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let connector_f_vout = 1 + 2 * graph.parameters.watchtower_pubkeys.len() as u64; - if outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, connector_f_vout) + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + let connector_e_vout = + output_topology::watchtower_challenge_init::connector_e(watchtower_num) as u64; + let connector_f_vout = + output_topology::watchtower_challenge_init::connector_f(watchtower_num) as u64; + if outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, connector_e_vout) .await? .is_some() + || outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, connector_f_vout) + .await? + .is_some() { tracing::warn!( - "Ignore OperatorCommitBlockHashTimeout for {instance_id}:{graph_id}: connector_F already spent" - ); - return Ok(()); - } - let watchtower_challenge_init_height = match ctx - .btc_client - .get_tx_status(&watchtower_challenge_init_txid) - .await? - .block_height - { - Some(height) => height, - None => { - tracing::warn!( - "Ignore OperatorCommitBlockHashTimeout for {instance_id}:{graph_id}: watchtower challenge init tx not confirmed yet" - ); - return Ok(()); - } - }; - let current_height = ctx.btc_client.get_height().await?; - if current_height - < watchtower_challenge_init_height + commit_blockhash_timeout_timelock(get_network()) - { - tracing::warn!( - "Ignore OperatorCommitBlockHashTimeout for {instance_id}:{graph_id}: commit-blockhash timelock not expired yet" + "Ignore OperatorCommitPubinTimeout for {instance_id}:{graph_id}: timeout input already spent" ); return Ok(()); } - let connector_g_vout = 2 * graph.parameters.watchtower_pubkeys.len() as u64; - if outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, connector_g_vout) - .await? - .is_some() - { - tracing::warn!( - "Ignore OperatorCommitBlockHashTimeout for {instance_id}:{graph_id}: connector_G already spent" - ); + + let timeout_tx = graph.operator_commit_timeout.tx().clone(); + let timeout_txid = timeout_tx.compute_txid(); + if tx_on_chain(ctx.btc_client, &timeout_txid).await? { return Ok(()); } - // 1. broadcast OperatorCommitBlockHashTimeout txn - let blockhash_commit_timeout_tx = graph.blockhash_commit_timeout.finalize(); - let anchor_vout = blockhash_commit_timeout_tx.output.len() as u64 - 1; - let blockhash_commit_timeout_tx_total_input_amount = - graph.blockhash_commit_timeout.prev_outs().iter().map(|o| o.value).sum(); - let child_tx = build_cpfp_txns( - ctx.btc_client, - &blockhash_commit_timeout_tx, - anchor_vout, - blockhash_commit_timeout_tx_total_input_amount, - ) - .await?; - match child_tx { - Some(tx) => { - broadcast_package(ctx.btc_client, &[blockhash_commit_timeout_tx, tx], true).await? - } - None => broadcast_tx(ctx.btc_client, &blockhash_commit_timeout_tx).await?, - }; + let timeout_tx_total_input_amount = + graph.operator_commit_timeout.prev_outs().iter().map(|o| o.value).sum::(); + broadcast_tx_with_cpfp(ctx.btc_client, timeout_tx, timeout_tx_total_input_amount).await?; + tracing::info!( + "Operator commit pubin timeout sent for {instance_id}:{graph_id}: txid={timeout_txid}" + ); Ok(()) } +// after the watchtower challenge flow is complete, build proof and broadcast Assert transaction. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_assert_init_ready_operator( +async fn handle_assert_ready_operator( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - content: &GOATMessageContent, ) -> Result<()> { - // triggered by timeout task - let message = make_message(ctx, content); - let (mut graph, graph_status, graph_sub_status) = match refresh_graph_status( - ctx, + let message = GOATMessage::new( + Actor::Operator, + GOATMessageContent::AssertReady(AssertReady { instance_id, graph_id }), + ); + let (mut graph, _graph_status, _graph_sub_status) = + match refresh_graph_status(ctx, instance_id, graph_id, None, GraphStatus::Challenge).await? + { + Some(v) => v, + None => return Ok(()), + }; + let (operator_wrapper_proof, wait_secs) = get_operator_wrapper_proof( + ctx.local_db, + ctx.http_client, + &graph, + ctx.btc_client, instance_id, graph_id, - Some(&message), - GraphStatus::Challenge, ) - .await? + .await?; + + if wait_secs > 0 { + tracing::info!( + "Retry AssertReady later for {instance_id}:{graph_id}: operator proof is not ready" + ); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, wait_secs).await?; + return Ok(()); + } + + let Some(operator_wrapper_proof) = operator_wrapper_proof else { + return Ok(()); + }; + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let assert_secret_key = operator_master_key.assert_wots_keypair_for_graph(graph_id).0; + let assert_witness = build_assert_witness(&operator_wrapper_proof.proof, &assert_secret_key)?; + let assert_message = assert_wots_message(&assert_witness)?; + let mut asserted_wrapper_proof = Vec::new(); + operator_wrapper_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; + let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; + let operator_state = setup_state + .operator + .as_mut() + .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; + if let Some(existing) = &operator_state.asserted_wrapper_proof + && existing != &asserted_wrapper_proof { - Some(v) => v, - None => return Ok(()), + bail!("operator assertion wrapper proof conflicts with persisted proof"); + } + operator_state.asserted_wrapper_proof = Some(asserted_wrapper_proof); + save_babe_setup_state(ctx.local_db, instance_id, graph_id, &setup_state)?; + + let assert_tx = operator_sign_assert(&mut graph, &assert_secret_key, &assert_message)?; + broadcast_nonstandard_tx(ctx.btc_client, &assert_tx).await?; + + let message_content = GOATMessageContent::AssertSent(AssertSent { + instance_id, + graph_id, + assert_txid: assert_tx.compute_txid(), + assert_witness: Some(assert_witness), + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Verifier, message_content)).await?; + + Ok(()) +} + +// verify Operator DynamicPublicInput and Proof; broadcast PubinDisprove or ChallengeAssert as needed. +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_assert_sent_verifier( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + assert_txid: Txid, + assert_witness: &Option, +) -> Result<()> { + // TODO: check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction + let (graph, _graph_status, _graph_sub_status) = + match refresh_graph_status(ctx, instance_id, graph_id, None, GraphStatus::Challenge).await? + { + Some(v) => v, + None => return Ok(()), + }; + + let Some(assert_tx) = ctx.btc_client.get_tx(&assert_txid).await? else { + tracing::warn!( + "Ignore AssertSent for {instance_id}:{graph_id}: assert tx {assert_txid} not found on chain" + ); + return Ok(()); }; - if graph_status != GraphStatus::Challenge { + let Some(assert_witness) = assert_witness else { + // TODO(gc-v2): recover BabeAssertWitness from the assert tx witness and graph. tracing::warn!( - "Ignore AssertInitReady for {instance_id}:{graph_id}: graph status is {graph_status:?}" + "Ignore AssertSent for {instance_id}:{graph_id}: assert witness is not available in message" + ); + return Ok(()); + }; + + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let verifier_pubkey = verifier_master_key.master_keypair().public_key().into(); + let Some(verifier_index) = + find_verifier_index_by_pubkey(&graph.parameters.gc_data, &verifier_pubkey)? + else { + tracing::debug!( + "Ignore AssertSent for {instance_id}:{graph_id}: local verifier has no graph slot" + ); + return Ok(()); + }; + validate_verifier_slot(&graph, verifier_index)?; + + let Some(saved_verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .and_then(|state| state.verifier) + else { + tracing::warn!( + "Ignore AssertSent for {instance_id}:{graph_id}: missing BABE verifier setup state" + ); + return Ok(()); + }; + if saved_verifier_state.verifier_index != Some(verifier_index) { + tracing::warn!( + "Ignore AssertSent for {instance_id}:{graph_id}: local setup slot does not match graph owner slot" ); return Ok(()); } - match graph_sub_status { - Some(sub_status) => { - if !sub_status.is_watchtower_challenge_success() { - tracing::warn!( - "Ignore AssertInitReady for {instance_id}:{graph_id}: watchtower challenge not finished yet, sub-status {sub_status:?}" - ); - return Ok(()); - } - } - None => { - tracing::warn!( - "Ignore AssertInitReady for {instance_id}:{graph_id}: graph sub-status is None" - ); - return Ok(()); - } - } - let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); - let operator_graph_keypair = operator_master_key.master_keypair(); - let assert_init_txid = graph.assert_init.tx().compute_txid(); - // uncomment the following lines to check operator proof before sending assert-init txn - // let (proof_opt, wait_secs) = - // get_operator_proof(local_db, http_client, instance_id, graph_id).await?; - // if proof_opt.is_none() { - // tracing::warn!( - // "Retry AssertInitReady for {instance_id}:{graph_id} later: operator proof not ready, retry after {wait_secs} seconds" - // ); - // push_local_unhandled_messages(local_db, graph_id, &message, wait_secs).await?; - // return Ok(()); - // } - // 1. sign & broadcast assert-init txn - if !tx_on_chain(ctx.btc_client, &assert_init_txid).await? { - let assert_init_tx = operator_sign_assert_init(operator_graph_keypair, &mut graph)?; - let anchor_vout = assert_init_tx.output.len() as u64 - 1; - let assert_init_tx_total_input_amount = - graph.assert_init.prev_outs().iter().map(|o| o.value).sum(); - let child_tx = build_cpfp_txns( - ctx.btc_client, - &assert_init_tx, - anchor_vout, - assert_init_tx_total_input_amount, - ) - .await?; - match child_tx { - Some(tx) => broadcast_package(ctx.btc_client, &[assert_init_tx, tx], true).await?, - None => broadcast_tx(ctx.btc_client, &assert_init_tx).await?, + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE challenge")?; + let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let challenge_witness = build_real_challenge_assert_witness( + &saved_verifier_state.private_state, + &saved_verifier_state.setup_package, + &saved_verifier_state.finalized_indices, + &vk, + &public_inputs, + &graph.parameters.operator_assert_wots_pubkey, + assert_witness, + verifier_index, + )?; + let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .input_labels + .iter() + .map(|label| label.to_vec()) + .collect::>() + .try_into() + .map_err(|labels: Vec>| { + anyhow!( + "BABE challenge witness exposes {} labels; expected {}", + labels.len(), + goat::assert_scripts::INPUT_WIRE_NUM + ) + })?; + let operator_assert_txin = assert_tx + .input + .first() + .cloned() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; + let challenge_assert_tx = + build_verifier_assert_tx(&graph, operator_assert_txin, verifier_index, labels)?; + broadcast_nonstandard_tx(ctx.btc_client, &challenge_assert_tx).await?; + let message_content = GOATMessageContent::ChallengeAssertSent(ChallengeAssertSent { + instance_id, + graph_id, + challenge_assert_txid: challenge_assert_tx.compute_txid(), + verifier_index, + challenge_witness: Some(challenge_witness), + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Operator, message_content)).await?; + + Ok(()) +} + +// compute msg after ChallengeAssert is broadcast and broadcast WronglyChallenge transaction. +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_challenge_assert_sent_operator( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + challenge_assert_txid: Txid, + verifier_index: usize, + challenge_witness: &Option, + content: &GOATMessageContent, +) -> Result<()> { + let (graph, _graph_status, _graph_sub_status) = + match refresh_graph_status(ctx, instance_id, graph_id, None, GraphStatus::Challenge).await? + { + Some(v) => v, + None => return Ok(()), }; - // assert-commit should be broadcasted after assert-init is confirmed (wait for 1 block) - let delay_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()); - push_local_unhandled_messages(ctx.local_db, graph_id, &message, delay_secs as usize) - .await?; - return Ok(()); - } - let connector_d_vout = graph.assert_commit_timeout_txns.len() as u64; - if outpoint_spent_txid(ctx.btc_client, &assert_init_txid, connector_d_vout).await?.is_some() { + let Some(challenge_witness) = challenge_witness else { + // TODO(gc-v2): recover BabeChallengeAssertWitness from the challenge tx witness and graph. tracing::warn!( - "Ignore AssertInitReady for {instance_id}:{graph_id}: connector_D already spent" + "Ignore ChallengeAssertSent for {instance_id}:{graph_id}: challenge witness is not available in message" ); return Ok(()); - } - // 2. sign & broadcast assert-commit txns - if !tx_confirmed(ctx.btc_client, &assert_init_txid).await? { - // assert-commit should be broadcasted after assert-init is confirmed (wait for 1 block) + }; + + validate_verifier_slot(&graph, verifier_index)?; + validate_challenge_witness_index(verifier_index, challenge_witness)?; + validate_expected_challenge_assert_txid(&graph, verifier_index, challenge_assert_txid)?; + + let Some(challenge_assert_tx) = ctx.btc_client.get_tx(&challenge_assert_txid).await? else { let delay_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()); + let message = make_message(ctx, content); push_local_unhandled_messages(ctx.local_db, graph_id, &message, delay_secs as usize) .await?; + tracing::info!( + "Retry ChallengeAssertSent later for {instance_id}:{graph_id}: challenge assert tx {challenge_assert_txid} not found on chain" + ); return Ok(()); - } else { - let (split_txid_opt, has_pending_fee_input, proof_wait_secs_opt) = - operator_send_assert_commit(ctx.local_db, ctx.btc_client, ctx.http_client, &mut graph) - .await?; - if let Some(wait_secs) = proof_wait_secs_opt { - tracing::warn!( - "Retry AssertInitReady for {instance_id}:{graph_id} later: operator proof not ready, retry after {wait_secs} seconds" - ); - push_local_unhandled_messages(ctx.local_db, graph_id, &message, wait_secs).await?; - return Ok(()); - } else if let Some(split_txid) = split_txid_opt { - let delay_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()) * 2; - tracing::warn!( - "Retry AssertInitReady for {instance_id}:{graph_id} later: fee_inputs_split_tx {split_txid} broadcasted, retry after {delay_secs} seconds" - ); - push_local_unhandled_messages(ctx.local_db, graph_id, &message, delay_secs as usize) - .await?; - } else if has_pending_fee_input { - let delay_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()) * 2; - tracing::warn!( - "Retry AssertInitReady for {instance_id}:{graph_id} later: some fee inputs are pending, retry after {delay_secs} seconds", - ); - push_local_unhandled_messages(ctx.local_db, graph_id, &message, delay_secs as usize) - .await?; - } + }; + + let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .input_labels + .iter() + .map(|label| label.to_vec()) + .collect::>() + .try_into() + .map_err(|labels: Vec>| { + anyhow!( + "BABE challenge witness exposes {} labels; expected {}", + labels.len(), + goat::assert_scripts::INPUT_WIRE_NUM + ) + })?; + let operator_assert_txid = challenge_assert_tx + .input + .first() + .ok_or_else(|| anyhow!("published verifier assert transaction has no input"))? + .previous_output + .txid; + let operator_assert_tx = + ctx.btc_client.get_tx(&operator_assert_txid).await?.ok_or_else(|| { + anyhow!("published operator assert transaction {operator_assert_txid} is unavailable") + })?; + let operator_assert_txin = operator_assert_tx + .input + .first() + .cloned() + .ok_or_else(|| anyhow!("published operator assert transaction has no input"))?; + let expected_challenge_assert = + build_verifier_assert_tx(&graph, operator_assert_txin, verifier_index, labels)?; + if expected_challenge_assert.input.first().map(|input| &input.witness) + != challenge_assert_tx.input.first().map(|input| &input.witness) + { + bail!("published verifier assert witness does not match BABE challenge labels"); } + + let setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; + let operator_state = setup_state + .operator + .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; + let candidate = operator_state + .candidates + .iter() + .find(|candidate| candidate.verifier_index == Some(verifier_index)) + .ok_or_else(|| anyhow!("missing BABE prover state for verifier slot {verifier_index}"))?; + let prover_state = candidate + .prover_state + .as_ref() + .ok_or_else(|| anyhow!("missing BABE prover state for verifier slot {verifier_index}"))?; + let proof_bytes = operator_state + .asserted_wrapper_proof + .as_ref() + .ok_or_else(|| anyhow!("missing asserted wrapper proof for graph {graph_id}"))?; + let proof = Groth16Proof::deserialize_compressed(proof_bytes.as_slice()) + .context("deserialize asserted wrapper proof")?; + let wrongly_challenged_witness = + recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof)?; + // TODO: The wrongly-challenged script accepts any one finalized-message preimage. + let final_msg = wrongly_challenged_witness + .final_msgs + .first() + .ok_or_else(|| anyhow!("wrongly challenged witness has no final message preimage"))?; + let (wrongly_challenged_input, _amount) = + operator_sign_wrongly_challenged(&graph, verifier_index, final_msg)?; + let wrongly_challenged_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![wrongly_challenged_input], + output: vec![goat::scripts::p2a_output()], + }; + broadcast_nonstandard_tx(ctx.btc_client, &wrongly_challenged_tx).await?; + Ok(()) } +// broadcast NoWithdraw after the ChallengeAssert timelock expires. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_assert_commit_timeout_challenger( +async fn handle_wrongly_challenge_timeout_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, + challenge_assert_txid: Txid, + verifier_index: usize, content: &GOATMessageContent, ) -> Result<()> { - // triggered by timeout task - let message = make_message(ctx, content); - let (graph, graph_status, _graph_sub_status) = match refresh_graph_status( + let (graph, _graph_status, _graph_sub_status) = match refresh_graph_status( ctx, instance_id, graph_id, - Some(&message), - GraphStatus::Challenge, + None, + GraphStatus::Disprove, ) .await? { Some(v) => v, None => return Ok(()), }; - if graph_status != GraphStatus::Challenge { - tracing::warn!( - "Ignore AssertCommitTimeout for {instance_id}:{graph_id}: graph status is {graph_status:?}" + validate_verifier_slot(&graph, verifier_index)?; + validate_expected_challenge_assert_txid(&graph, verifier_index, challenge_assert_txid)?; + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let local_verifier_pubkey: PublicKey = verifier_master_key.master_keypair().public_key().into(); + if graph.parameters.gc_data[verifier_index].verifier_pubkey != local_verifier_pubkey { + tracing::debug!( + "Ignore WronglyChallengeTimeout for {instance_id}:{graph_id}: local verifier does not own slot {verifier_index}" ); return Ok(()); } - let assert_init_txid = graph.assert_init.tx().compute_txid(); - let connector_d_vout = graph.assert_commit_timeout_txns.len() as u64; - if outpoint_spent_txid(ctx.btc_client, &assert_init_txid, connector_d_vout).await?.is_some() { - tracing::warn!( - "Ignore AssertCommitTimeout for {instance_id}:{graph_id}: connector_D already spent" + + let delay_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()); + let message = make_message(ctx, content); + if ctx.btc_client.get_tx(&challenge_assert_txid).await?.is_none() { + push_local_unhandled_messages(ctx.local_db, graph_id, &message, delay_secs as usize) + .await?; + tracing::info!( + "Retry WronglyChallengeTimeout later for {instance_id}:{graph_id}: challenge assert tx {challenge_assert_txid} not found on chain" ); return Ok(()); } - let assert_init_height = match ctx + + let challenge_assert_height = match ctx .btc_client - .get_tx_status(&assert_init_txid) + .get_tx_status(&challenge_assert_txid) .await? .block_height { - Some(height) => height, + Some(height) => height as u64, None => { - tracing::warn!( - "Ignore AssertCommitTimeout for {instance_id}:{graph_id}: assert init tx not confirmed yet" + push_local_unhandled_messages(ctx.local_db, graph_id, &message, delay_secs as usize) + .await?; + tracing::info!( + "Retry WronglyChallengeTimeout later for {instance_id}:{graph_id}: challenge assert tx {challenge_assert_txid} is not confirmed" ); return Ok(()); } }; - let current_height = ctx.btc_client.get_height().await?; - if current_height < assert_init_height + assert_commit_timeout_timelock(get_network()) { - tracing::warn!( - "Ignore AssertCommitTimeout for {instance_id}:{graph_id}: assert init tx timelock not expired yet" + + let disprove_height = challenge_assert_height + + disprove_timelock(graph.parameters.instance_parameters.network) as u64; + let goat_confirmed_height = ctx.goat_client.btc_spv_latest_height().await?; + if goat_confirmed_height < disprove_height { + let retry_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()) + * (disprove_height - goat_confirmed_height); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, retry_secs as usize) + .await?; + tracing::info!( + "Retry WronglyChallengeTimeout later for {instance_id}:{graph_id}: disprove timelock has not expired" ); return Ok(()); } - let mut commit_index = None; - for i in 0..graph.assert_commit_timeout_txns.len() { - let assert_commit_vout = i as u64; - if outpoint_spent_txid(ctx.btc_client, &assert_init_txid, assert_commit_vout) - .await? - .is_none() - { - commit_index = Some(i); - break; - } - } - let commit_index = match commit_index { - Some(index) => index, - None => { - tracing::warn!( - "Ignore AssertCommitTimeout for {instance_id}:{graph_id}: all assert commit connectors already spent" - ); - return Ok(()); - } - }; - // 1. broadcast AssertCommitTimeout txn - let assert_commit_timeout_tx = graph - .assert_commit_timeout_txns - .get(commit_index) - .ok_or_else(|| { - anyhow!("AssertCommitTimeout txn not found for {instance_id}:{graph_id}:{commit_index}") - })? - .finalize(); - let anchor_vout = assert_commit_timeout_tx.output.len() as u64 - 1; - let assert_commit_timeout_tx_total_input_amount = - graph.assert_commit_timeout_txns[commit_index].prev_outs().iter().map(|o| o.value).sum(); - let child_tx = build_cpfp_txns( - ctx.btc_client, - &assert_commit_timeout_tx, - anchor_vout, - assert_commit_timeout_tx_total_input_amount, - ) - .await?; - match child_tx { - Some(tx) => { - broadcast_package(ctx.btc_client, &[assert_commit_timeout_tx, tx], true).await? - } - None => broadcast_tx(ctx.btc_client, &assert_commit_timeout_tx).await?, - }; - Ok(()) -} - -#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_disprove_ready_challenger( - ctx: &mut HandlerContext<'_>, - instance_id: Uuid, - graph_id: Uuid, - content: &GOATMessageContent, -) -> Result<()> { - // triggered by AssertCommit tx or OperatorCommitBlockHash tx - let message = make_message(ctx, content); - let (graph, graph_status, _graph_sub_status) = match refresh_graph_status( - ctx, - instance_id, - graph_id, - Some(&message), - GraphStatus::Challenge, - ) - .await? + if let Some(spent_txid) = outpoint_spent_txid(ctx.btc_client, &challenge_assert_txid, 0).await? { - Some(v) => v, - None => return Ok(()), - }; - if graph_status != GraphStatus::Challenge { - tracing::warn!( - "Ignore DisproveReady for {instance_id}:{graph_id}: graph status is {graph_status:?}" + tracing::info!( + "Skip Disprove for {instance_id}:{graph_id} slot {verifier_index}: verifier assertion output already spent by {spent_txid}" ); return Ok(()); } - // 1. get assertions committed by Operator from Bitcoin chain - let operator_commit_blockhash_txin = { - let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let connector_g_vout = 2 * graph.parameters.watchtower_pubkeys.len() as u64; - let commit_blockhash_timeout_txid = graph.blockhash_commit_timeout.tx().compute_txid(); - match outpoint_spent_txin(ctx.btc_client, &watchtower_challenge_init_txid, connector_g_vout) - .await? - { - Some((spent_txid, _, txin)) => { - if spent_txid == commit_blockhash_timeout_txid { - tracing::warn!( - "Ignore DisproveReady for {instance_id}:{graph_id}: graph already challenged by CommitBlockHashTimeout: {spent_txid}" - ); - return Ok(()); - } - txin - } - None => { - tracing::warn!( - "Ignore DisproveReady for {instance_id}:{graph_id}: operator-commit-blockhash not sent yet" - ); - return Ok(()); - } - } - }; - let operator_assert_commit_txins = { - let assert_init_txid = graph.assert_init.tx().compute_txid(); - let mut txins = vec![]; - for i in 0..graph.assert_commit_timeout_txns.len() { - let assert_commit_vout = i as u64; - match outpoint_spent_txin(ctx.btc_client, &assert_init_txid, assert_commit_vout).await? - { - Some((spent_txid, _, txin)) => { - let assert_commit_timeout_txid = - graph.assert_commit_timeout_txns[i].tx().compute_txid(); - if spent_txid == assert_commit_timeout_txid { - tracing::warn!( - "Ignore DisproveReady for {instance_id}:{graph_id}: graph already challenged by AssertCommitTimeout[{i}]: {spent_txid}" - ); - return Ok(()); - } - txins.push(txin); - } - None => { - tracing::warn!( - "Ignore DisproveReady for {instance_id}:{graph_id}: assert-commit {i} not sent yet" - ); - return Ok(()); - } - } - } - txins - }; - let operator_ack_txins = { - let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let mut txins = vec![]; - for watchtower_index in 0..graph.parameters.watchtower_pubkeys.len() { - let ack_vout = 1 + 2 * watchtower_index as u64; - if let Some((spent_txid, _, txin)) = - outpoint_spent_txin(ctx.btc_client, &watchtower_challenge_init_txid, ack_vout) - .await? - { - let nack_txid = graph.nack_txns[watchtower_index].tx().compute_txid(); - if spent_txid != nack_txid { - txins.push(txin); - } - } - } - txins - }; - // 2. check assertions committed by Operator, if any assertion is invalid, sign & broadcast disprove txn - let vk = crate::vk::get_vk().await?; - let disprove_scripts = get_disprove_scripts(&graph.parameters).await?; - let disprove_scripts = - disprove_scripts.try_into().map_err(|_| anyhow!("Mismatch disprove scripts num"))?; - if let Some(disprove_witness) = verify_operator_commits( - operator_commit_blockhash_txin, - operator_assert_commit_txins, - operator_ack_txins, - graph.parameters.watchtower_pubkeys.len(), - &vk, - &disprove_scripts, - )? { - let disprover_evm_address = get_node_goat_address() - .ok_or_else(|| anyhow::anyhow!("failed to get node goat address".to_string()))?; - let connector_e_input = Input { - outpoint: OutPoint { txid: graph.kickoff.tx().compute_txid(), vout: 3 }, - amount: graph.kickoff.tx().output[3].value, - }; - let disprove_tx = sign_disprove( - &graph, - &connector_e_input, - disprove_witness, - disprove_scripts.to_vec(), - Some(*disprover_evm_address.as_ref()), - )?; - let challenger_master_key = ChallengerMasterKey::new(get_bitvm_key()?); - let challenger_disprove_keypair = challenger_master_key.keypair_for_nst_disprove(); - if let Err(e) = build_sign_and_broadcast_non_standard_tx( - ctx.btc_client, - challenger_disprove_keypair, - disprove_tx.clone(), - connector_e_input.amount, - ) - .await - { - if e.downcast_ref::() - .is_some_and(|se| matches!(se, SpecialError::InsufficientBalance(_))) - { - let disprove_address = node_p2wsh_address( - get_network(), - &challenger_disprove_keypair.public_key().into(), - ); - let disprove_balance = ctx - .btc_client - .get_address_utxo(disprove_address.clone()) - .await? - .iter() - .map(|u| u.value) - .sum::(); - let fee_rate = get_fee_rate(ctx.btc_client).await?; - let est_fee_sat = - ((disprove_tx.weight().to_vbytes_ceil() + 200) as f64 * fee_rate).ceil() as u64; - let target_balance_sat = est_fee_sat + 20_000; - let shortfall_sat = target_balance_sat.saturating_sub(disprove_balance.to_sat()); - if shortfall_sat > 0 { - tracing::info!( - "Top up nst-disprove p2wsh address for {instance_id}:{graph_id}: shortfall={} sats", - shortfall_sat - ); - fund_address( - ctx.btc_client, - challenger_master_key.master_keypair(), - disprove_address, - bitcoin::Amount::from_sat(shortfall_sat), - ) - .await?; - } - build_sign_and_broadcast_non_standard_tx( - ctx.btc_client, - challenger_disprove_keypair, - disprove_tx, - connector_e_input.amount, - ) - .await?; - } else { - return Err(e); - } - } - } else { - tracing::info!("All assertions valid for {instance_id}:{graph_id}, no need to disprove"); - return Ok(()); - } + + let disprove_tx = build_disprove_tx(&graph, verifier_index, None)?; + broadcast_nonstandard_tx(ctx.btc_client, &disprove_tx).await?; + + let message_content = GOATMessageContent::DisproveSent(DisproveSent { + instance_id, + graph_id, + disprove_type: DisproveTxType::Disprove, + index: verifier_index, + challenge_start_txid: Some(challenge_assert_txid), + challenge_finish_txid: disprove_tx.compute_txid(), + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Committee, message_content)).await?; + Ok(()) } -#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id +))] async fn handle_disprove_sent_committee( ctx: &mut HandlerContext<'_>, instance_id: Uuid, @@ -3349,7 +4061,7 @@ async fn handle_disprove_sent_committee( } let kickoff_txid = graph.kickoff.tx().compute_txid(); let take1_txid = graph.take1.tx().compute_txid(); - let connector_a_vout = 0; + let connector_a_vout = output_topology::kickoff::connector_a() as u64; let challenge_start_tx = if let Some(spent_txid) = outpoint_spent_txid(ctx.btc_client, &kickoff_txid, connector_a_vout).await? { @@ -3372,79 +4084,6 @@ async fn handle_disprove_sent_committee( return Ok(()); } }; - match disprove_type { - DisproveTxType::AssertTimeout => { - if challenge_finish_txid - != graph - .assert_commit_timeout_txns - .get(index) - .ok_or_else(|| { - anyhow!( - "AssertCommitTimeout txn not found for {instance_id}:{graph_id}:{index}" - ) - })? - .tx() - .compute_txid() - { - tracing::warn!( - "Ignore DisproveSent for {instance_id}:{graph_id}: challenge finish txid does not match assert commit timeout txn" - ); - return Ok(()); - } - } - DisproveTxType::OperatorCommitTimeout => { - if challenge_finish_txid != graph.blockhash_commit_timeout.tx().compute_txid() { - tracing::warn!( - "Ignore DisproveSent for {instance_id}:{graph_id}: challenge finish txid does not match operator commit timeout txn" - ); - return Ok(()); - } - } - DisproveTxType::OperatorNack => { - if challenge_finish_txid - != graph - .nack_txns - .get(index) - .ok_or_else(|| { - anyhow!("Nack txn not found for {instance_id}:{graph_id}:{index}") - })? - .tx() - .compute_txid() - { - tracing::warn!( - "Ignore DisproveSent for {instance_id}:{graph_id}: challenge finish txid does not match nack txn" - ); - return Ok(()); - } - } - DisproveTxType::Disprove => { - let connector_e_input = OutPoint { txid: kickoff_txid, vout: 3 }; - if challenge_finish_tx.input[0].previous_output != connector_e_input { - tracing::warn!( - "Ignore DisproveSent for {instance_id}:{graph_id}: challenge finish tx is not a disprove txn" - ); - return Ok(()); - } - } - DisproveTxType::QuickChallenge => { - let guardian_connector_input = OutPoint { txid: kickoff_txid, vout: 4 }; - if challenge_finish_tx.input[0].previous_output != guardian_connector_input { - tracing::warn!( - "Ignore DisproveSent for {instance_id}:{graph_id}: challenge finish tx is not a quick challenge txn" - ); - return Ok(()); - } - } - DisproveTxType::ChallengeIncompleteKickoff => { - let guardian_connector_input = OutPoint { txid: kickoff_txid, vout: 4 }; - if challenge_finish_tx.input[0].previous_output != guardian_connector_input { - tracing::warn!( - "Ignore DisproveSent for {instance_id}:{graph_id}: challenge finish tx is not a challenge incomplete kickoff txn" - ); - return Ok(()); - } - } - } let challenge_finish_height = match ctx .btc_client .get_tx_status(&challenge_finish_txid) @@ -3526,8 +4165,8 @@ async fn handle_take1_ready_operator( return Ok(()); } let kickoff_txid = graph.kickoff.tx().compute_txid(); - let connector_a_vout = 0; - let guardian_connector_vout = 4; + let connector_a_vout = output_topology::kickoff::connector_a() as u64; + let guardian_connector_vout = output_topology::kickoff::guardian_connector() as u64; if outpoint_spent_txid(ctx.btc_client, &kickoff_txid, connector_a_vout).await?.is_some() || outpoint_spent_txid(ctx.btc_client, &kickoff_txid, guardian_connector_vout) .await? @@ -3684,62 +4323,40 @@ async fn handle_take2_ready_operator( ); return Ok(()); } - let kickoff_txid = graph.kickoff.tx().compute_txid(); - let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let assert_init_txid = graph.assert_init.tx().compute_txid(); - let connector_d_vout = graph.assert_commit_timeout_txns.len() as u64; - let connector_e_vout = 3; - let connector_f_vout = 1 + 2 * graph.parameters.watchtower_pubkeys.len() as u64; - let guardian_connector_vout = 4; - // check if connector_E, connector_F, connector_D, guardian_connector are all unspent - if outpoint_spent_txid(ctx.btc_client, &kickoff_txid, connector_e_vout).await?.is_some() - || outpoint_spent_txid(ctx.btc_client, &watchtower_challenge_init_txid, connector_f_vout) - .await? - .is_some() - || outpoint_spent_txid(ctx.btc_client, &assert_init_txid, connector_d_vout).await?.is_some() - || outpoint_spent_txid(ctx.btc_client, &kickoff_txid, guardian_connector_vout) - .await? - .is_some() - { - tracing::warn!("Ignore Take2Ready for {instance_id}:{graph_id}: connectors already spent"); - return Ok(()); - } - // check if assert-init tx and watchtower-challenge-init tx are both confirmed and timelock expired - let assert_init_height = match ctx - .btc_client - .get_tx_status(&assert_init_txid) - .await? - .block_height - { - Some(height) => height, - None => { + let operator_assert_txid = graph.operator_assert.tx().compute_txid(); + // Take2 spends Connector-0, Connector-D, and Guardian Connector. Recheck every input before + // signing so a stale Take2Ready cannot race an already-spent connector. + let take2_inputs = + graph.take2.tx().input.iter().map(|txin| txin.previous_output).collect::>(); + for previous_output in take2_inputs { + if let Some(spent_txid) = + outpoint_spent_txid(ctx.btc_client, &previous_output.txid, previous_output.vout as u64) + .await? + { tracing::warn!( - "Ignore Take2Ready for {instance_id}:{graph_id}: assert init tx not confirmed yet" + "Ignore Take2Ready for {instance_id}:{graph_id}: take2 input {}:{} already spent by {}", + previous_output.txid, + previous_output.vout, + spent_txid ); return Ok(()); } - }; - let watchtower_challenge_init_height = match ctx + } + let operator_assert_height = match ctx .btc_client - .get_tx_status(&watchtower_challenge_init_txid) + .get_tx_status(&operator_assert_txid) .await? .block_height { Some(height) => height, None => { tracing::warn!( - "Ignore Take2Ready for {instance_id}:{graph_id}: watchtower challenge init tx not confirmed yet" + "Ignore Take2Ready for {instance_id}:{graph_id}: assert init tx not confirmed yet" ); return Ok(()); } }; - if !is_take2_timelock_expired( - ctx.btc_client, - watchtower_challenge_init_height, - assert_init_height, - ) - .await? - { + if !is_take2_timelock_expired(ctx.btc_client, operator_assert_height).await? { tracing::warn!( "Ignore Take2Ready for {instance_id}:{graph_id}: take2 timelock not expired yet" ); @@ -3880,7 +4497,7 @@ async fn handle_sync_graph( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - graph: &SimplifiedBitvm2Graph, + graph: &SimplifiedBitvmGcGraph, ) -> Result<()> { // sent by relayer nodes in response to SyncGraphRequest if graph_exists(ctx.local_db, instance_id, graph_id).await? { @@ -3895,7 +4512,7 @@ async fn handle_sync_graph( ) })?; store_graph(ctx.local_db, graph).await?; - let graph = Bitvm2Graph::from_simplified(graph)?; + let graph = BitvmGcGraph::from_simplified(graph)?; refresh_and_compensate( ctx, instance_id, @@ -3925,3 +4542,127 @@ async fn handle_response_node_info( save_node_info(ctx.local_db, node_info).await?; Ok(()) } + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use bitvm_lib::babe_adapter::{ + BABE_M_CC, BabeProverState, build_setup_package, open_and_solder, + }; + + use super::*; + + fn verifier_pubkey() -> PublicKey { + PublicKey::from_str("0279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798") + .unwrap() + } + + fn gc_submission() -> (CACSetupPackage, BitvmGcCircuitData, BabeProverState) { + let package = build_setup_package(BABE_M_CC + 1).unwrap(); + let selected = (0..BABE_M_CC).collect::>(); + let (_, finalized, soldering) = open_and_solder(&package, &selected).unwrap(); + let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()).unwrap(); + let prover_state = BabeProverState { + package: package.clone(), + finalized, + soldering, + h_msgs: gc_data.final_msg_hashlocks.clone(), + }; + (package, gc_data, prover_state) + } + + fn operator_state(package: CACSetupPackage) -> OperatorBabeSetupState { + OperatorBabeSetupState { + frozen_verifier_pubkeys: Some(vec![verifier_pubkey()]), + candidates: vec![OperatorVerifierCandidate { + verifier_pubkey: verifier_pubkey(), + setup_package: package, + verifier_index: Some(0), + selected_circuit_indexes: (0..BABE_M_CC).collect(), + gc_data: None, + prover_state: None, + }], + asserted_wrapper_proof: None, + } + } + + #[test] + fn freeze_operator_candidate_selects_protocol_finalized_count() { + let package = build_setup_package(BABE_M_CC + 1).unwrap(); + let mut state = OperatorBabeSetupState { + frozen_verifier_pubkeys: None, + candidates: vec![OperatorVerifierCandidate { + verifier_pubkey: verifier_pubkey(), + setup_package: package, + verifier_index: None, + selected_circuit_indexes: vec![], + gc_data: None, + prover_state: None, + }], + asserted_wrapper_proof: None, + }; + + freeze_operator_candidates(&mut state).unwrap(); + + assert_eq!(state.candidates[0].selected_circuit_indexes.len(), BABE_M_CC); + } + + #[test] + fn candidate_records_one_slot_and_full_prover_state_idempotently() { + let (package, gc_data, prover_state) = gc_submission(); + let mut state = operator_state(package.clone()); + + let graph_data = record_candidate_gc_data( + &mut state, + verifier_pubkey(), + 0, + &package, + gc_data.clone(), + prover_state.clone(), + ) + .unwrap() + .unwrap(); + + assert_eq!(graph_data.len(), 1); + assert_eq!(graph_data[0].final_msg_hashlocks.len(), BABE_M_CC); + assert_eq!(state.candidates[0].prover_state.as_ref().unwrap().finalized.len(), BABE_M_CC); + + let duplicate = record_candidate_gc_data( + &mut state, + verifier_pubkey(), + 0, + &package, + gc_data.clone(), + prover_state.clone(), + ) + .unwrap() + .unwrap(); + assert_eq!(duplicate.len(), 1); + + let mut conflict = prover_state; + conflict.h_msgs[0][0] ^= 1; + assert!( + record_candidate_gc_data( + &mut state, + verifier_pubkey(), + 0, + &package, + gc_data, + conflict, + ) + .is_err() + ); + } + + #[test] + fn legacy_operator_candidate_without_prover_state_deserializes() { + let (package, _, _) = gc_submission(); + let mut value = serde_json::to_value(operator_state(package)).unwrap(); + value["candidates"][0].as_object_mut().unwrap().remove("prover_state"); + + let restored: OperatorBabeSetupState = serde_json::from_value(value).unwrap(); + + assert!(restored.candidates[0].prover_state.is_none()); + } +} diff --git a/node/src/lib.rs b/node/src/lib.rs index 9ecafd89..fe677b09 100644 --- a/node/src/lib.rs +++ b/node/src/lib.rs @@ -7,6 +7,7 @@ pub mod p2p_msg_handler; pub mod rpc_service; mod scheduled_tasks; +mod soldering_payload_store; pub mod utils; pub use scheduled_tasks::{ run_maintenance_tasks, run_sequencer_set_hash_monitor_task, run_watch_event_task, @@ -14,200 +15,3 @@ pub use scheduled_tasks::{ mod error; mod vk; - -mod dbg { - #![allow(unused)] - use core::panic; - use std::str::FromStr; - - use crate::action::*; - use crate::utils::*; - use bitcoin::{Address, Amount, Network, Witness}; - use bitvm2_lib::actors::Actor; - use bitvm2_lib::committee::*; - use bitvm2_lib::keys::*; - use bitvm2_lib::operator::*; - use bitvm2_lib::types::*; - use goat::connectors::connector_e::ConnectorE; - use goat::connectors::kickoff_connectors::*; - use goat::contexts::base::generate_n_of_n_public_key; - use goat::disprove_scripts::hash160; - use goat::scripts::p2a_script; - use goat::transactions::base::Input; - use goat::transactions::prekickoff::PrekickoffTransaction; - use secp256k1::{Keypair, Secp256k1, SecretKey, rand}; - use serde::Deserialize; - use serde::Serialize; - use uuid::Uuid; - - fn dbg_network() -> Network { - Network::Testnet4 - } - fn dbg_keypair() -> Keypair { - let secp = Secp256k1::new(); - Keypair::new(&secp, &mut rand::thread_rng()) - } - fn dbg_input() -> Input { - Input { - outpoint: bitcoin::OutPoint { - txid: bitcoin::Txid::from_str( - "b2b18acfdd358369d9a1e8370cfd9eb4ee123507a0321ad608de500cc54740d8", - ) - .unwrap(), - vout: 0, - }, - amount: Amount::from_sat(500000000), - } - } - fn dbg_address() -> Address { - node_p2wsh_address(dbg_network(), &dbg_keypair().public_key().into()) - } - fn build_dbg_instance_parameters() -> Bitvm2InstanceParameters { - let user_info = UserInfo { - depositor_evm_address: [1u8; 20], - txn_fees: [1000u64; 3], - inputs: vec![dbg_input()], - user_xonly_pubkey: dbg_keypair().public_key().x_only_public_key().0, - user_change_address: dbg_address(), - user_refund_address: dbg_address(), - }; - let committee_pubkeys = - vec![dbg_keypair().public_key().into(), dbg_keypair().public_key().into()]; - let committee_agg_pubkey = generate_n_of_n_public_key(&committee_pubkeys).0; - Bitvm2InstanceParameters { - network: dbg_network(), - instance_id: Uuid::new_v4(), - user_info, - pegin_amount: Amount::from_sat(1000000), - committee_pubkeys, - committee_agg_pubkey, - } - } - fn build_dbg_prekickoff_parameters() -> PrekickoffParameters { - let xonly_pubkey = dbg_keypair().public_key().x_only_public_key().0; - let force_skip_connector = ForceSkipConnector::new(dbg_network(), &xonly_pubkey); - let kickoff_connector = KickoffConnector::new(dbg_network(), &xonly_pubkey); - let prekickoff_connector = PrekickoffConnector::new(dbg_network(), &xonly_pubkey); - let cur_prekickoff_txn = PrekickoffTransaction::new_for_validation( - &prekickoff_connector, - &force_skip_connector, - &kickoff_connector, - &prekickoff_connector, - dbg_input(), - vec![], - vec![], - 1000, - 2, - 50, - ) - .unwrap(); - PrekickoffParameters { - cur_prekickoff_txn, - replenish_fee_inputs: vec![], - replenish_fee_prev_outs: vec![], - fee_amount: 1000, - } - } - fn build_dbg_graph_parameters() -> Bitvm2GraphParameters { - let graph_id = Uuid::new_v4(); - let instance_parameters = build_dbg_instance_parameters(); - let prekickoff_parameters = build_dbg_prekickoff_parameters(); - let operator_master_key = OperatorMasterKey::new(dbg_keypair()); - let operator_master_keypair = operator_master_key.master_keypair(); - let operator_pubkey = operator_master_keypair.public_key().into(); - let operator_wots_pubkeys = operator_master_key.wots_keypair_for_graph(graph_id).1; - let watchtower_pubkeys = vec![ - dbg_keypair().public_key().x_only_public_key().0, - dbg_keypair().public_key().x_only_public_key().0, - ]; - let mut hashlocks = vec![]; - for index in 0..watchtower_pubkeys.len() { - let preimage = b"preimage".to_vec(); - let hashlock = hash160(&preimage); - hashlocks.push(hashlock); - } - let instance_id = instance_parameters.instance_id; - Bitvm2GraphParameters { - instance_parameters, - prekickoff_parameters, - graph_id, - graph_nonce: 1, - challenge_amount: todo_funcs::challenge_amount(), - operator_pubkey, - operator_wots_pubkeys, - operator_receive_address: dbg_address(), - watchtower_pubkeys, - hashlocks, - guest_constant_value: [3u8; 32], - } - } - fn build_dbg_simplified_graph() -> SimplifiedBitvm2Graph { - let disprove_scripts = vec![p2a_script()]; - let graph = generate_bitvm_graph(build_dbg_graph_parameters(), disprove_scripts).unwrap(); - graph.to_simplified().unwrap() - } - - #[tokio::test] - async fn dbg_serde() { - let graph: SimplifiedBitvm2Graph = build_dbg_simplified_graph(); - let message_content = GOATMessageContent::CreateGraph(CreateGraph { - instance_id: graph.parameters.instance_parameters.instance_id, - graph_id: graph.parameters.graph_id, - graph_nonce: graph.parameters.graph_nonce, - graph: graph.clone(), - }); - let msg = GOATMessage::new(Actor::All, message_content); - let msg_se = msg.serialize_message().await.unwrap(); - let msg_de = GOATMessage::deserialize_message(&msg_se).await.unwrap(); - // check graph id - assert_eq!( - graph.parameters.graph_id, - match msg_de.content { - GOATMessageContent::CreateGraph(ref cg) => cg.graph_id, - _ => Uuid::nil(), - } - ); - } - - #[test] - fn dbg_serde_ignores_legacy_graph_zkm_version() { - let graph = build_dbg_simplified_graph(); - let mut value = serde_json::to_value(&graph).unwrap(); - value - .get_mut("parameters") - .and_then(serde_json::Value::as_object_mut) - .unwrap() - .insert("zkm_version".to_string(), serde_json::Value::String("v1.2.5".to_string())); - - let decoded: SimplifiedBitvm2Graph = serde_json::from_value(value).unwrap(); - assert_eq!(decoded.parameters.graph_id, graph.parameters.graph_id); - assert_eq!(decoded.parameters.graph_nonce, graph.parameters.graph_nonce); - } - - #[tokio::test] - #[ignore = "requires local db"] - async fn dbg_serde_from_db() { - let dbg_path = "/home/ubuntu/bitvm2-noded-test/operator_0/bitvm2-node.db"; - let instance_id = uuid::Uuid::parse_str("A4DB2DD03EEA43FB9601D60236EBAD90").unwrap(); - let graph_id = uuid::Uuid::parse_str("044285C328EE4D2EAE06718659EFDC34").unwrap(); - let local_db = store::create_local_db(dbg_path).await; - let graph = get_graph(&local_db, instance_id, graph_id).await.unwrap().unwrap(); - let message_content = GOATMessageContent::CreateGraph(CreateGraph { - instance_id: graph.parameters.instance_parameters.instance_id, - graph_id: graph.parameters.graph_id, - graph_nonce: graph.parameters.graph_nonce, - graph, - }); - let msg = GOATMessage::new(Actor::All, message_content); - let msg_se = msg.serialize_message().await.unwrap(); - let msg_de = GOATMessage::deserialize_message(&msg_se).await.unwrap(); - // check graph id - assert_eq!( - graph_id, - match msg_de.content { - GOATMessageContent::CreateGraph(ref cg) => cg.graph_id, - _ => Uuid::nil(), - } - ); - } -} diff --git a/node/src/main.rs b/node/src/main.rs index 9945701f..4a87fb48 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -1,9 +1,11 @@ #![feature(trivial_bounds)] use base64::Engine; -use bitvm2_lib::actors::Actor; -use bitvm2_noded::env::{ +use bitvm_lib::actors::Actor; +use bitvm_lib::babe_adapter::BabeBundleBuilder; +use bitvm_noded::env::{ self, ENV_PEER_KEY, SEQUENCER_SET_MONITOR_INTERVAL_SECS, check_node_info, get_btc_url_from_env, get_goat_network, get_network, get_node_pubkey, goat_config_from_env, + validate_soldering_proof_payload_store_config, }; use clap::{Parser, Subcommand}; use client::{btc_chain::BTCClient, goat_chain::GOATClient}; @@ -13,16 +15,16 @@ use std::error::Error; use std::sync::{Arc, Mutex}; use tracing_subscriber::EnvFilter; -use bitvm2_noded::utils::{ +use bitvm_noded::utils::{ self, generate_local_key, save_local_info, set_node_external_socket_addr_env, }; -use bitvm2_noded::{ +use bitvm_noded::{ rpc_service, run_maintenance_tasks, run_sequencer_set_hash_monitor_task, run_watch_event_task, }; use anyhow::Result; -use bitvm2_noded::middleware::swarm::{Bitvm2SwarmConfig, BitvmNetworkManager}; -use bitvm2_noded::p2p_msg_handler::BitvmNodeProcessor; +use bitvm_noded::middleware::swarm::{BitvmNetworkManager, BitvmSwarmConfig}; +use bitvm_noded::p2p_msg_handler::BitvmNodeProcessor; use client::http_client::async_client::HttpAsyncClient; use futures::future; use tokio::signal; @@ -41,7 +43,7 @@ struct Opts { pub rpc_addr: String, /// Local Sqlite database file path - #[arg(long, default_value = "sqlite:/tmp/bitvm2-node.db")] + #[arg(long, default_value = "sqlite:/tmp/bitvm-node.db")] pub db_path: String, /// Peer nodes as the bootnodes @@ -52,7 +54,7 @@ struct Opts { #[arg(long, default_value = "/metrics")] metrics_path: String, - /// Whether to run the libp2p Kademlia protocol and join the BitVM2 DHT. + /// Whether to run the libp2p Kademlia protocol and join the BitVM DHT. #[arg(long, default_value = "true")] enable_kademlia: bool, @@ -114,12 +116,13 @@ async fn main() -> Result<(), Box> { KeyCommands::FundingAddress => { let public_key = get_node_pubkey()?; let p2wsh_addr = utils::node_p2wsh_address(get_network(), &public_key); - println!("Funding P2WSH address (for operator and challenger): {p2wsh_addr}"); + println!("Funding P2WSH address (for operator and verifier): {p2wsh_addr}"); } } return Ok(()); } let _ = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).try_init(); + validate_soldering_proof_payload_store_config(&actor)?; let is_publisher = actor == Actor::Publisher || actor == Actor::All; let sequencer_set_monitor_start_cosmos_block = @@ -135,15 +138,15 @@ async fn main() -> Result<(), Box> { // Create cancellation token for graceful shutdown let cancellation_token = CancellationToken::new(); let mut task_handles: Vec>> = vec![]; - // init bitvm2swarm + // init bitvmswarm let bitvm_network_manager = BitvmNetworkManager::new( - Bitvm2SwarmConfig { + BitvmSwarmConfig { local_key: env::get_peer_key(), p2p_port: opt.p2p_port, bootnodes: opt.bootnodes, topic_names: vec![ Actor::Committee.to_string(), - Actor::Challenger.to_string(), + Actor::Verifier.to_string(), Actor::Operator.to_string(), Actor::Watchtower.to_string(), Actor::All.to_string(), @@ -160,6 +163,8 @@ async fn main() -> Result<(), Box> { btc_client: BTCClient::new(get_network(), get_btc_url_from_env().as_deref()), goat_client: GOATClient::new(env::goat_config_from_env().await, env::get_goat_network()), http_client: HttpAsyncClient::new(None), + soldering_builder: matches!(actor, Actor::Verifier | Actor::Operator) + .then(|| Arc::new(BabeBundleBuilder::new())), }; let actor_clone1 = actor.clone(); diff --git a/node/src/middleware/behaviour.rs b/node/src/middleware/behaviour.rs index 7080e0cc..f7773fe7 100644 --- a/node/src/middleware/behaviour.rs +++ b/node/src/middleware/behaviour.rs @@ -1,4 +1,3 @@ -// #![feature(trivial_bounds)] use libp2p::identity::Keypair; use libp2p::{gossipsub, kad, kad::store::MemoryStore, swarm::StreamProtocol}; use libp2p_swarm_derive::NetworkBehaviour; diff --git a/node/src/middleware/swarm.rs b/node/src/middleware/swarm.rs index d4e7d57f..ee9b5ae4 100644 --- a/node/src/middleware/swarm.rs +++ b/node/src/middleware/swarm.rs @@ -3,7 +3,7 @@ use crate::middleware::{AllBehaviours, split_topic_name}; use crate::{env, middleware}; use anyhow::bail; use base64::Engine; -use bitvm2_lib::actors::Actor; +use bitvm_lib::actors::Actor; use futures::StreamExt; use libp2p::gossipsub::MessageId; use libp2p::multiaddr::Protocol; @@ -92,7 +92,7 @@ pub trait P2pMessageHandler { } #[derive(Clone, Debug)] -pub struct Bitvm2SwarmConfig { +pub struct BitvmSwarmConfig { pub local_key: String, pub p2p_port: u16, pub bootnodes: Vec, @@ -102,13 +102,13 @@ pub struct Bitvm2SwarmConfig { } pub struct BitvmNetworkManager { - config: Bitvm2SwarmConfig, + config: BitvmSwarmConfig, peer_id: PeerId, swarm: BitvmSwarmWrapper, } impl BitvmNetworkManager { pub fn new( - config: Bitvm2SwarmConfig, + config: BitvmSwarmConfig, metric_registry: &mut Registry, ) -> anyhow::Result { let key_pair = libp2p::identity::Keypair::from_protobuf_encoding(&Zeroizing::new( @@ -204,13 +204,27 @@ impl BitvmNetworkManager { match event { SwarmEvent::NewListenAddr { address, .. } => tracing::debug!("Listening on {address:?}"), SwarmEvent::Behaviour(AllBehavioursEvent::Gossipsub(gossipsub::Event::Message { - propagation_source: _peer_id, + propagation_source, message_id: id, message, })) => { + let source = message.source.unwrap_or(propagation_source); + let data_prefix = hex::encode(&message.data[..message.data.len().min(16)]); + let data_starts_with_goatbin = message.data.starts_with(b"GOATBIN1"); match msg_handler.recv_and_dispatch(&mut self.swarm, actor.clone(), - message.source.expect("empty message source"), id, &message.data).await { - Ok(_) => {},Err(e) => { tracing::error!("Fail to handle p2p message, error: {e:?}") } + source, id.clone(), &message.data).await { + Ok(_) => {},Err(e) => { + tracing::error!( + error = ?e, + from_peer_id = %source, + message_id = ?id, + topic = %message.topic, + data_len = message.data.len(), + data_prefix, + data_starts_with_goatbin, + "Fail to handle p2p message" + ) + } } } SwarmEvent::Behaviour(AllBehavioursEvent::Gossipsub(gossipsub::Event::Subscribed { peer_id, topic})) => { diff --git a/node/src/p2p_msg_handler.rs b/node/src/p2p_msg_handler.rs index 868f3ed5..be31343a 100644 --- a/node/src/p2p_msg_handler.rs +++ b/node/src/p2p_msg_handler.rs @@ -4,11 +4,13 @@ use crate::action::{ use crate::env::get_local_node_info; use crate::middleware::swarm::{BitvmSwarmWrapper, P2pMessageHandler, TickMessageType}; use crate::utils::detect_heart_beat; -use bitvm2_lib::actors::Actor; +use bitvm_lib::actors::Actor; +use bitvm_lib::babe_adapter::BabeBundleBuilder; use client::http_client::async_client::HttpAsyncClient; use client::{btc_chain::BTCClient, goat_chain::GOATClient}; use libp2p::PeerId; use libp2p::gossipsub::MessageId; +use std::sync::Arc; use store::localdb::LocalDB; pub struct BitvmNodeProcessor { @@ -16,6 +18,7 @@ pub struct BitvmNodeProcessor { pub btc_client: BTCClient, pub goat_client: GOATClient, pub http_client: HttpAsyncClient, + pub soldering_builder: Option>, } impl P2pMessageHandler for BitvmNodeProcessor { async fn recv_and_dispatch( @@ -32,6 +35,7 @@ impl P2pMessageHandler for BitvmNodeProcessor { &self.btc_client, &self.goat_client, &self.http_client, + &self.soldering_builder, actor, from_peer_id, id, @@ -71,6 +75,7 @@ impl P2pMessageHandler for BitvmNodeProcessor { &self.btc_client, &self.goat_client, &self.http_client, + &self.soldering_builder, actor, peer_id, GOATMessage::default_message_id(), @@ -105,12 +110,12 @@ mod tests { use crate::action::{GOATMessage, GOATMessageContent, NodeInfo, send_to_peer}; use crate::env::get_rpc_support_actors; use crate::middleware::swarm::{ - Bitvm2SwarmConfig, BitvmNetworkManager, BitvmSwarmWrapper, P2pMessageHandler, + BitvmNetworkManager, BitvmSwarmConfig, BitvmSwarmWrapper, P2pMessageHandler, TickMessageType, }; use crate::utils::{generate_local_key, save_node_info}; use base64::Engine; - use bitvm2_lib::actors::Actor; + use bitvm_lib::actors::Actor; use libp2p::PeerId; use libp2p::gossipsub::MessageId; use prometheus_client::registry::Registry; @@ -147,13 +152,13 @@ mod tests { local_key }; let mut bitvm_network_manager = BitvmNetworkManager::new( - Bitvm2SwarmConfig { + BitvmSwarmConfig { local_key, p2p_port, bootnodes, topic_names: vec![ Actor::Committee.to_string(), - Actor::Challenger.to_string(), + Actor::Verifier.to_string(), Actor::Operator.to_string(), Actor::Watchtower.to_string(), Actor::All.to_string(), @@ -163,7 +168,7 @@ mod tests { }, &mut metric_registry, ) - .expect("create bitvm2 swarm"); + .expect("create bitvm swarm"); let local_db = if let Some(local_db) = local_db { local_db diff --git a/node/src/rpc_service/auth.rs b/node/src/rpc_service/auth.rs index 25d47067..4d5db476 100644 --- a/node/src/rpc_service/auth.rs +++ b/node/src/rpc_service/auth.rs @@ -9,7 +9,7 @@ use sha2::{Digest, Sha256}; pub const AUTH_TIMESTAMP_HEADER: &str = "x-auth-timestamp"; pub const AUTH_SIGNATURE_HEADER: &str = "x-auth-signature"; const AUTH_WINDOW_SECS: i64 = 300; -const AUTH_DOMAIN: &[u8] = b"bitvm2-auth"; +const AUTH_DOMAIN: &[u8] = b"bitvm-auth"; /// Verify that the request carries a valid Schnorr signature produced by the /// same `BITVM_SECRET` this node is configured with. @@ -17,7 +17,7 @@ const AUTH_DOMAIN: &[u8] = b"bitvm2-auth"; /// Expected headers: /// - `X-Auth-Timestamp`: current unix epoch seconds (string) /// - `X-Auth-Signature`: hex-encoded 64-byte Schnorr signature of -/// `SHA256(b"bitvm2-auth" || timestamp_str)` +/// `SHA256(b"bitvm-auth" || timestamp_str)` pub fn verify_request_auth(headers: &HeaderMap) -> Result<(), (StatusCode, Json)> { let timestamp_str = headers .get(AUTH_TIMESTAMP_HEADER) @@ -65,7 +65,7 @@ pub fn verify_request_auth(headers: &HeaderMap) -> Result<(), (StatusCode, Json< /// Build the `(x-auth-timestamp, x-auth-signature)` header values for a request. /// -/// Signs `SHA256(b"bitvm2-auth" || timestamp_str)` with the provided keypair using +/// Signs `SHA256(b"bitvm-auth" || timestamp_str)` with the provided keypair using /// Schnorr and returns `(timestamp_str, hex_signature)`. pub fn sign_request_auth(keypair: &Keypair) -> (String, String) { let timestamp = std::time::SystemTime::now() diff --git a/node/src/rpc_service/bitvm2.rs b/node/src/rpc_service/bitvm.rs similarity index 97% rename from node/src/rpc_service/bitvm2.rs rename to node/src/rpc_service/bitvm.rs index 5dee94df..15f0fc72 100644 --- a/node/src/rpc_service/bitvm2.rs +++ b/node/src/rpc_service/bitvm.rs @@ -1,7 +1,7 @@ use super::utils::{deserialize_u256, serialize_u256}; use crate::rpc_service::current_time_secs; use crate::scheduled_tasks::graph_maintenance_tasks::{ - AssertCommitStatus, ChallengeSubStatus, WatchtowerChallengeStatus, + ChallengeSubStatus, VerifierChallengeStatus, }; use crate::utils::{check_bridge_in_uxto_available_or_self_spent, reflect_goat_address}; use alloy::hex::ToHexExt; @@ -22,14 +22,22 @@ use strum::{Display, EnumString}; use tracing::warn; use uuid::Uuid; +#[allow(dead_code)] pub const WATCHTOWER_CHALLENGE_STEP_INIT: &str = "Watchtower Challenge init"; +#[allow(dead_code)] pub const WATCHTOWER_CHALLENGE_STEP_CHALLENGE: &str = "Watchtower Challenge"; +#[allow(dead_code)] pub const WATCHTOWER_CHALLENGE_STEP_CHALLENGE_TIMEOUT: &str = "Watchtower Challenge Timeout"; +#[allow(dead_code)] pub const WATCHTOWER_CHALLENGE_STEP_ACK: &str = "Operator Challenge ACK"; +#[allow(dead_code)] pub const WATCHTOWER_CHALLENGE_STEP_COMMIT_BLOCKHASH: &str = "Operator Commit BlockHash"; +#[allow(dead_code)] pub const WATCHTOWER_CHALLENGE_STEP_COMMIT_BLOCKHASH_TIMEOUT: &str = "Operator Commit BlockHash Timeout"; +#[allow(dead_code)] pub const ASSERT_STEP_INIT: &str = "Assert init"; +#[allow(dead_code)] pub const ASSERT_STEP_COMMIT: &str = "Assert Commit"; const BRIDGE_IN_FAIL_AS_UTXO_BEEN_SPENT: &str = "Your UTXO has already been spent."; @@ -112,6 +120,7 @@ pub struct PegoutResponse { #[derive(Debug, Deserialize)] pub struct GraphTxGetParams { pub tx_name: String, + pub index: Option, } #[derive(Debug, Deserialize)] @@ -486,14 +495,19 @@ pub type GraphGetResponse = GraphExtended; #[derive(Deserialize, Serialize, Default)] pub struct GraphTxnGetResponse { - pub assert_init: BtcTxData, - pub watchtower_challenge_init: BtcTxData, - pub pre_kickoff: BtcTxData, - pub challenge: BtcTxData, - pub disprove: BtcTxData, - pub kickoff: BtcTxData, + pub cur_prekickoff: BtcTxData, + pub next_prekickoff: BtcTxData, + pub force_skip_kickoff: BtcTxData, + pub quick_challenge: BtcTxData, + pub challenge_incomplete_kickoff: BtcTxData, pub pegin: BtcTxData, + pub kickoff: BtcTxData, pub take1: BtcTxData, + pub challenge: BtcTxData, + pub watchtower_challenge_init: BtcTxData, + pub operator_assert: BtcTxData, + pub verifier_asserts: Vec, + pub disproves: Vec, pub take2: BtcTxData, } #[derive(Deserialize, Serialize, Default)] @@ -658,9 +672,12 @@ impl GraphExtended { let challenge_sub_status = match serde_json::from_str::(&graph.sub_status) { Ok(v) => { - if v.assert_commit_status != AssertCommitStatus::None { + if v.verifier_challenge_status + .iter() + .any(|status| *status != VerifierChallengeStatus::None) + { SimpleChallengeSubStatus::Assert - } else if v.watchtower_challenge_status != WatchtowerChallengeStatus::None { + } else if v.watchtower_challenge_status.iter().any(|status| *status) { SimpleChallengeSubStatus::WatchtowerChallenge } else { SimpleChallengeSubStatus::None diff --git a/node/src/rpc_service/handler/bitvm2_handler.rs b/node/src/rpc_service/handler/bitvm2_handler.rs index 86448b72..f3a6c5ae 100644 --- a/node/src/rpc_service/handler/bitvm2_handler.rs +++ b/node/src/rpc_service/handler/bitvm2_handler.rs @@ -4,16 +4,13 @@ use crate::env::{ get_node_goat_address, get_node_pubkey, }; use crate::rpc_service::auth::verify_request_auth; -use crate::rpc_service::bitvm2::*; +use crate::rpc_service::bitvm::*; use crate::rpc_service::node::ALIVE_TIME_JUDGE_THRESHOLD; use crate::rpc_service::response::{ ApiErrorExt, ApiResult, ErrorResponse, error_response, ok_response, }; use crate::rpc_service::validation::InputValidator; use crate::rpc_service::{AppState, current_time_secs}; -use crate::scheduled_tasks::graph_maintenance_tasks::{ - AssertInitTxVoutMonitorData, ChallengeSubStatus, WTInitTxVoutMonitorData, -}; use crate::utils::{ find_instances_by_escrow_hash, gen_instance_parameters_local, get_bridge_out_global_stats, parse_graph_raw_data, send_challenge_tx, @@ -22,8 +19,8 @@ use alloy::primitives::{Address, U256}; use axum::Json; use axum::extract::{Path, Query, State}; use bitcoin::consensus::encode::serialize_hex; -use bitvm2_lib::types::{Bitvm2Graph, SimplifiedBitvm2Graph}; -use client::goat_chain::{DisproveTxType, PeginStatus, WithdrawStatus}; +use bitvm_lib::types::{BitvmGcGraph, SimplifiedBitvmGcGraph}; +use client::goat_chain::{PeginStatus, WithdrawStatus}; use goat::transactions::pre_signed::PreSignedTransaction; use http::{HeaderMap, StatusCode}; use sha2::{Digest, Sha256}; @@ -61,6 +58,20 @@ fn bridge_out_retry_jitter_ms(attempt: u32) -> u64 { base + (now % 25) } +fn graph_tx_index_error( + tx_name: GraphBtcTxName, + index: usize, + len: usize, +) -> (StatusCode, Json) { + ( + StatusCode::BAD_REQUEST, + Json(ErrorResponse { + error: "INVALID_GRAPH_TX_INDEX".to_string(), + message: format!("Invalid index {index} for {tx_name}. Available count: {len}"), + }), + ) +} + /// Get instance settings /// /// Returns bridge-in amount configuration information for frontend display of available bridge amount options. @@ -781,12 +792,12 @@ pub async fn get_instances_overview( /// Get graph by ID /// -/// Returns detailed information for a specific BitVM2 graph including transaction status +/// Returns detailed information for a specific BitVM graph including transaction status /// and waiting time information. /// /// # Path Parameters /// -/// - `graph_id`: UUID of the BitVM2 graph to retrieve +/// - `graph_id`: UUID of the BitVM graph to retrieve /// /// # Returns /// @@ -796,7 +807,7 @@ pub async fn get_instances_overview( /// /// # Use Case /// -/// Applications use this to retrieve detailed information about a specific BitVM2 graph, +/// Applications use this to retrieve detailed information about a specific BitVM graph, /// including its current status and estimated waiting time. /// /// # Example @@ -829,13 +840,13 @@ pub async fn get_instances_overview( /// "take1_txid": null, /// "challenge_txid": null, /// "take2_txid": null, -/// "disprove_txid": null, /// "watchtower_challenge_init_txid": null, +/// "operator_assert_txid": null, +/// "verifier_assert_txids": [], +/// "disprove_txids": [], /// "watchtower_challenge_timeout_txids": [], -/// "nack_txids": [], -/// "blockhash_commit_timeout_txid": null, -/// "assert_init_txid": null, -/// "assert_commit_timeout_txids": [], +/// "operator_challenge_nack_txids": [], +/// "operator_commit_timeout_txid": null, /// "init_withdraw_tx_hash": null, /// "bridge_out_start_at": 1699123456, /// "status_updated_at": 1699123456, @@ -921,13 +932,13 @@ pub async fn get_graph( /// "take1_txid": null, /// "challenge_txid": null, /// "take2_txid": null, -/// "disprove_txid": null, /// "watchtower_challenge_init_txid": null, +/// "operator_assert_txid": null, +/// "verifier_assert_txids": [], +/// "disprove_txids": [], /// "watchtower_challenge_timeout_txids": [], -/// "nack_txids": [], -/// "blockhash_commit_timeout_txid": null, -/// "assert_init_txid": null, -/// "assert_commit_timeout_txids": [], +/// "operator_challenge_nack_txids": [], +/// "operator_commit_timeout_txid": null, /// "init_withdraw_tx_hash": null, /// "bridge_out_start_at": 1699123456, /// "status_updated_at": 1699123456, @@ -973,7 +984,7 @@ pub async fn get_graphs( /// Get ready to kickoff graph /// -/// Returns a BitVM2 graph that is ready for the operator to kickoff. This endpoint is used by +/// Returns a BitVM graph that is ready for the operator to kickoff. This endpoint is used by /// operators to query available graphs that need kickoff processing. /// /// # Query Parameters @@ -1023,13 +1034,13 @@ pub async fn get_graphs( /// "take1_txid": null, /// "challenge_txid": null, /// "take2_txid": null, -/// "disprove_txid": null, /// "watchtower_challenge_init_txid": null, +/// "operator_assert_txid": null, +/// "verifier_assert_txids": [], +/// "disprove_txids": [], /// "watchtower_challenge_timeout_txids": [], -/// "nack_txids": [], -/// "blockhash_commit_timeout_txid": null, -/// "assert_init_txid": null, -/// "assert_commit_timeout_txids": [], +/// "operator_challenge_nack_txids": [], +/// "operator_commit_timeout_txid": null, /// "init_withdraw_tx_hash": null, /// "bridge_out_start_at": 0, /// "status_updated_at": 1699123456, @@ -1114,181 +1125,42 @@ pub async fn get_ready_to_kickoff_graph( /// Get graph Bitcoin transaction progress data /// -/// Helper function to retrieve progress tracking data for specific BitVM2 graph transactions. +/// Helper function to retrieve progress tracking data for specific BitVM graph transactions. /// This function monitors transaction vout status and extracts progress information for /// watchtower challenges and assert operations. /// /// # Parameters /// /// - `storage_processor`: Database storage processor for querying transaction monitoring data -/// - `btc_tx_name`: The type of Bitcoin transaction to query (WatchtowerChallengeInit or AssertInit) +/// - `btc_tx_name`: The type of Bitcoin transaction to query /// - `graph`: The graph instance containing transaction IDs /// /// # Returns /// /// - `Ok((progress_data_vec, fail_reason))`: Tuple of progress data steps and optional failure reason -/// - `Err`: Error if database query or JSON deserialization fails -/// -/// # Features -/// -/// - For WatchtowerChallengeInit: Tracks init, challenge, challenge timeout, NACK, commit blockhash, and timeout steps -/// - For AssertInit: Tracks init and commit steps -/// - Returns empty progress data for other transaction types -/// -/// # Note -/// -/// Progress data includes current/total counts for each step in multi-stage transaction processes. +/// - `Err`: Error if database query fails pub(crate) async fn get_graph_btc_tx_process_data<'a>( - storage_processor: &mut StorageProcessor<'a>, - btc_tx_name: GraphBtcTxName, - graph: &Graph, + _storage_processor: &mut StorageProcessor<'a>, + _btc_tx_name: GraphBtcTxName, + _graph: &Graph, ) -> anyhow::Result<(Vec, Option)> { - let mut progress_datas: Vec = vec![]; - // todo update fail reason - let mut fail_reason: Option = None; - - match btc_tx_name { - GraphBtcTxName::WatchtowerChallengeInit => { - if let Some(tx) = graph.watchtower_challenge_init_txid.clone() - && let Some(vout_monitor) = - storage_processor.find_graph_btc_tx_vout_monitor(&graph.graph_id, &tx).await? - && let Ok(monitor_data) = - serde_json::from_str::(&vout_monitor.monitor_data) - && let Ok(challenge_status) = - serde_json::from_str::(&graph.sub_status) - { - progress_datas.push(ProgressData { - name: WATCHTOWER_CHALLENGE_STEP_INIT.to_string(), - current: 1, - total: 1, - }); - let (challenge_current, challenge_total) = - monitor_data.get_challenge_process_desc(); - progress_datas.push(ProgressData { - name: WATCHTOWER_CHALLENGE_STEP_CHALLENGE.to_string(), - current: challenge_current, - total: challenge_total, - }); - - let (challenge_timeout_current, challenge_timeout_total) = - monitor_data.get_challenge_timeout_process_desc(); - progress_datas.push(ProgressData { - name: WATCHTOWER_CHALLENGE_STEP_CHALLENGE_TIMEOUT.to_string(), - current: challenge_timeout_current, - total: challenge_timeout_total, - }); - - let (ack_current, ack_total) = monitor_data.get_ack_process_desc(); - if ack_total > 0 { - progress_datas.push(ProgressData { - name: WATCHTOWER_CHALLENGE_STEP_ACK.to_string(), - current: ack_current, - total: ack_total, - }); - } - - let (block_hash_current, block_hash_total) = - monitor_data.get_commit_block_hash_desc(); - progress_datas.push(ProgressData { - name: WATCHTOWER_CHALLENGE_STEP_COMMIT_BLOCKHASH.to_string(), - current: block_hash_current, - total: block_hash_total, - }); - - let (current, total) = monitor_data.get_commit_block_hash_timeout_desc(); - progress_datas.push(ProgressData { - name: WATCHTOWER_CHALLENGE_STEP_COMMIT_BLOCKHASH_TIMEOUT.to_string(), - current, - total, - }); - - if let Some(disprove_type) = challenge_status.disprove_type { - match disprove_type { - DisproveTxType::OperatorCommitTimeout => { - fail_reason = - Some("The operator commits blockhash timeout".to_string()); - } - DisproveTxType::OperatorNack => { - let (challenge_timeout_num, nack_num) = ( - challenge_timeout_total - challenge_timeout_current, - ack_total - ack_current, - ); - - fail_reason = match (challenge_timeout_num > 0, nack_num > 0) { - (true, true) => Some(format!( - "The operator has {challenge_timeout_num} unsent Challenge-Timeout transactions, and {nack_num} unsent Ack transactions" - )), - (false, true) => Some(format!( - "The operator has {nack_num} unsent Ack transactions" - )), - (true, false) => Some(format!( - "The operator has {challenge_timeout_num} unsent Challenge-Timeout transactions" - )), - (false, false) => None, - }; - } - _ => {} - } - } - } else { - progress_datas.push(ProgressData { - name: WATCHTOWER_CHALLENGE_STEP_INIT.to_string(), - current: 0, - total: 1, - }); - } - } - GraphBtcTxName::AssertInit => { - if let Some(tx) = graph.assert_init_txid.clone() - && let Some(vout_monitor) = - storage_processor.find_graph_btc_tx_vout_monitor(&graph.graph_id, &tx).await? - && let Ok(monitor_data) = - serde_json::from_str::(&vout_monitor.monitor_data) - && let Ok(challenge_status) = - serde_json::from_str::(&graph.sub_status) - { - progress_datas.push(ProgressData { - name: ASSERT_STEP_INIT.to_string(), - current: 1, - total: 1, - }); - let (current, total) = monitor_data.get_commit_process_desc(); - progress_datas.push(ProgressData { - name: ASSERT_STEP_COMMIT.to_string(), - current, - total, - }); - - if let Some(DisproveTxType::AssertTimeout) = challenge_status.disprove_type { - fail_reason = - Some(format!("The operator has {} unsent assertions", total - current)); - } - } else { - progress_datas.push(ProgressData { - name: ASSERT_STEP_INIT.to_string(), - current: 0, - total: 1, - }); - } - } - _ => {} - } - Ok((progress_datas, fail_reason)) + Ok((vec![], None)) } /// Get graph transaction by name /// /// Returns raw Bitcoin transaction data and progress information for a specific transaction -/// within a BitVM2 graph. Supports querying various transaction types including kickoff, +/// within a BitVM graph. Supports querying various transaction types including kickoff, /// challenge, and take transactions. /// /// # Path Parameters /// -/// - `graph_id`: UUID of the BitVM2 graph +/// - `graph_id`: UUID of the BitVM graph /// /// # Query Parameters /// -/// - `tx_name`: Name of the transaction to retrieve (e.g., "kickoff", "challenge", "take1", etc.) +/// - `tx_name`: Name of the transaction to retrieve, using the `*.hex` names from `GraphBtcTxName` +/// - `index`: Optional verifier index for `verifier-assert.hex` and `disprove.hex` /// /// # Returns /// @@ -1312,38 +1184,7 @@ pub(crate) async fn get_graph_btc_tx_process_data<'a>( /// { /// "btc_tx_data": { /// "raw_data": "020000000001...", -/// "progresses": [ -/// { -/// "name": "Watchtower Challenge init", -/// "current": 1, -/// "total": 1 -/// }, -/// { -/// "name": "Watchtower Challenge", -/// "current": 3, -/// "total": 5 -/// }, -/// { -/// "name": "Watchtower Challenge Timeout", -/// "current": 0, -/// "total": 2 -/// }, -/// { -/// "name": "Operator Challenge NACK", -/// "current": 2, -/// "total": 3 -/// }, -/// { -/// "name": "Operator Commit BlockHash", -/// "current": 1, -/// "total": 4 -/// }, -/// { -/// "name": "Operator Commit BlockHash Timeout", -/// "current": 0, -/// "total": 1 -/// } -/// ], +/// "progresses": [], /// "fail_reason": null /// } /// } @@ -1369,46 +1210,56 @@ pub async fn get_graph_tx( if let (Some(graph_raw_data), Some(graph)) = (graph_raw_data, graph) { let (progresses, fail_reason) = - get_graph_btc_tx_process_data(&mut storage_process, tx_name.clone(), &graph) + get_graph_btc_tx_process_data(&mut storage_process, tx_name, &graph) .await .api_error("GET_GRAPH_TX_ERROR")?; - let simplified_bitvm2_graph: SimplifiedBitvm2Graph = + let simplified_bitvm_graph: SimplifiedBitvmGcGraph = parse_graph_raw_data(graph_raw_data.raw_data.clone(), graph_id_uuid) .await .api_error("GET_GRAPH_TXN_ERROR")?; - let bitvm2_graph: Bitvm2Graph = Bitvm2Graph::from_simplified(&simplified_bitvm2_graph) + let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(&simplified_bitvm_graph) .api_error("GET_GRAPH_TX_ERROR")?; + let tx_index = params.index.unwrap_or(0); let raw_data = match tx_name { - GraphBtcTxName::AssertInit => serialize_hex(bitvm2_graph.assert_init.tx()), - GraphBtcTxName::PreKickoff => serialize_hex(bitvm2_graph.cur_prekickoff.tx()), - GraphBtcTxName::Kickoff => serialize_hex(bitvm2_graph.kickoff.tx()), - GraphBtcTxName::Pegin => serialize_hex(bitvm2_graph.pegin.tx()), - GraphBtcTxName::Take1 => serialize_hex(bitvm2_graph.take1.tx()), - GraphBtcTxName::Take2 => serialize_hex(bitvm2_graph.take2.tx()), - GraphBtcTxName::WatchtowerChallengeInit => { - serialize_hex(bitvm2_graph.watchtower_challenge_init.tx()) + GraphBtcTxName::CurPreKickoff => serialize_hex(bitvm_graph.cur_prekickoff.tx()), + GraphBtcTxName::NextPreKickoff => serialize_hex(bitvm_graph.next_prekickoff.tx()), + GraphBtcTxName::ForceSkipKickoff => serialize_hex(bitvm_graph.force_skip_kickoff.tx()), + GraphBtcTxName::QuickChallenge => serialize_hex(bitvm_graph.quick_challenge.tx()), + GraphBtcTxName::ChallengeIncompleteKickoff => { + serialize_hex(bitvm_graph.challenge_incomplete_kickoff.tx()) } + GraphBtcTxName::Pegin => serialize_hex(bitvm_graph.pegin.tx()), + GraphBtcTxName::Kickoff => serialize_hex(bitvm_graph.kickoff.tx()), + GraphBtcTxName::Take1 => serialize_hex(bitvm_graph.take1.tx()), GraphBtcTxName::Challenge => { if let Some(challenge_txid) = graph.challenge_txid && let Ok(Some(tx)) = app_state.btc_client.get_tx(&challenge_txid.0).await { serialize_hex(&tx) } else { - serialize_hex(bitvm2_graph.challenge.tx()) + serialize_hex(bitvm_graph.challenge.tx()) } } + GraphBtcTxName::WatchtowerChallengeInit => { + serialize_hex(bitvm_graph.watchtower_challenge_init.tx()) + } + GraphBtcTxName::OperatorAssert => serialize_hex(bitvm_graph.operator_assert.tx()), + GraphBtcTxName::VerifierAssert => { + let tx = bitvm_graph.verifier_asserts.get(tx_index).ok_or_else(|| { + graph_tx_index_error(tx_name, tx_index, bitvm_graph.verifier_asserts.len()) + })?; + serialize_hex(tx.tx()) + } GraphBtcTxName::Disprove => { - if let Some(disprove_txid) = graph.disprove_txid - && let Ok(Some(tx)) = app_state.btc_client.get_tx(&disprove_txid.0).await - { - serialize_hex(&tx) - } else { - "".to_string() - } + let tx = bitvm_graph.disproves.get(tx_index).ok_or_else(|| { + graph_tx_index_error(tx_name, tx_index, bitvm_graph.disproves.len()) + })?; + serialize_hex(tx.tx()) } + GraphBtcTxName::Take2 => serialize_hex(bitvm_graph.take2.tx()), }; ok_response(GraphTxGetResponse { @@ -1429,12 +1280,12 @@ pub async fn get_graph_tx( /// Get all graph transactions /// /// Returns raw Bitcoin transaction data and progress information for all transactions -/// in a BitVM2 graph. This includes all transaction types: assert_init, watchtower_challenge_init, -/// pre_kickoff, challenge, disprove, kickoff, pegin, take1, and take2. +/// in a BitVM graph. This includes all transaction types currently present in `BitvmGcGraph`, +/// including verifier assert and disprove vectors. /// /// # Path Parameters /// -/// - `graph_id`: UUID of the BitVM2 graph +/// - `graph_id`: UUID of the BitVM graph /// /// # Query Parameters /// @@ -1449,7 +1300,7 @@ pub async fn get_graph_tx( /// # Use Case /// /// Applications use this to retrieve all transaction details from a graph in a single request, -/// which is useful for displaying the complete transaction flow and status of a BitVM2 graph. +/// which is useful for displaying the complete transaction flow and status of a BitVM graph. /// /// # Example /// @@ -1460,82 +1311,20 @@ pub async fn get_graph_tx( /// Response example: /// ```json /// { -/// "assert_init": { -/// "raw_data": "020000000001...", -/// "progresses": [], -/// "fail_reason": null -/// }, -/// "watchtower_challenge_init": { -/// "raw_data": "020000000001...", -/// "progresses": [ -/// { -/// "name": "Watchtower Challenge init", -/// "current": 1, -/// "total": 1 -/// }, -/// { -/// "name": "Watchtower Challenge", -/// "current": 3, -/// "total": 5 -/// }, -/// { -/// "name": "Watchtower Challenge Timeout", -/// "current": 0, -/// "total": 2 -/// }, -/// { -/// "name": "Operator Challenge NACK", -/// "current": 2, -/// "total": 3 -/// }, -/// { -/// "name": "Operator Commit BlockHash", -/// "current": 1, -/// "total": 4 -/// }, -/// { -/// "name": "Operator Commit BlockHash Timeout", -/// "current": 0, -/// "total": 1 -/// } -/// ], -/// "fail_reason": null -/// }, -/// "pre_kickoff": { -/// "raw_data": "020000000001...", -/// "progresses": [], -/// "fail_reason": null -/// }, -/// "challenge": { -/// "raw_data": "020000000001...", -/// "progresses": [], -/// "fail_reason": null -/// }, -/// "disprove": { -/// "raw_data": "", -/// "progresses": [], -/// "fail_reason": null -/// }, -/// "kickoff": { -/// "raw_data": "020000000001...", -/// "progresses": [], -/// "fail_reason": null -/// }, -/// "pegin": { -/// "raw_data": "020000000001...", -/// "progresses": [], -/// "fail_reason": null -/// }, -/// "take1": { -/// "raw_data": "020000000001...", -/// "progresses": [], -/// "fail_reason": null -/// }, -/// "take2": { -/// "raw_data": "020000000001...", -/// "progresses": [], -/// "fail_reason": null -/// } +/// "cur_prekickoff": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "next_prekickoff": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "force_skip_kickoff": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "quick_challenge": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "challenge_incomplete_kickoff": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "pegin": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "kickoff": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "take1": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "challenge": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "watchtower_challenge_init": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "operator_assert": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null }, +/// "verifier_asserts": [{ "raw_data": "020000000001...", "progresses": [], "fail_reason": null }], +/// "disproves": [{ "raw_data": "020000000001...", "progresses": [], "fail_reason": null }], +/// "take2": { "raw_data": "020000000001...", "progresses": [], "fail_reason": null } /// } /// ``` #[axum::debug_handler] @@ -1595,11 +1384,11 @@ pub async fn get_graph_txn( }), ) })?; - let simplified_bitvm2_graph: SimplifiedBitvm2Graph = - parse_graph_raw_data(graph_raw_data.raw_data.clone(), graph_id_uuid) + let simplified_bitvm_graph: SimplifiedBitvmGcGraph = + parse_graph_raw_data(graph_raw_data.raw_data.clone(), graph.graph_id) .await .api_error("GET_GRAPH_TXN_ERROR")?; - let bitvm2_graph: Bitvm2Graph = Bitvm2Graph::from_simplified(&simplified_bitvm2_graph) + let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(&simplified_bitvm_graph) .api_error("GET_GRAPH_TXN_ERROR")?; let (wt_progresses, wt_fail_reason) = get_graph_btc_tx_process_data( @@ -1612,28 +1401,43 @@ pub async fn get_graph_txn( let (assert_progresses, assert_fail_reason) = get_graph_btc_tx_process_data( &mut storage_processor, - GraphBtcTxName::AssertInit, + GraphBtcTxName::OperatorAssert, &graph, ) .await .api_error("GET_GRAPH_TXN_ERROR")?; let mut resp = GraphTxnGetResponse { - assert_init: BtcTxData::new(serialize_hex(bitvm2_graph.assert_init.tx())) - .with_progresses(assert_progresses) - .with_fail_reason(assert_fail_reason), + cur_prekickoff: BtcTxData::new(serialize_hex(bitvm_graph.cur_prekickoff.tx())), + next_prekickoff: BtcTxData::new(serialize_hex(bitvm_graph.next_prekickoff.tx())), + force_skip_kickoff: BtcTxData::new(serialize_hex(bitvm_graph.force_skip_kickoff.tx())), + quick_challenge: BtcTxData::new(serialize_hex(bitvm_graph.quick_challenge.tx())), + challenge_incomplete_kickoff: BtcTxData::new(serialize_hex( + bitvm_graph.challenge_incomplete_kickoff.tx(), + )), + pegin: BtcTxData::new(serialize_hex(bitvm_graph.pegin.tx())), + kickoff: BtcTxData::new(serialize_hex(bitvm_graph.kickoff.tx())), + take1: BtcTxData::new(serialize_hex(bitvm_graph.take1.tx())), + challenge: BtcTxData::new(serialize_hex(bitvm_graph.challenge.tx())), watchtower_challenge_init: BtcTxData::new(serialize_hex( - bitvm2_graph.watchtower_challenge_init.tx(), + bitvm_graph.watchtower_challenge_init.tx(), )) .with_progresses(wt_progresses) .with_fail_reason(wt_fail_reason), - pre_kickoff: BtcTxData::new(serialize_hex(bitvm2_graph.cur_prekickoff.tx())), - challenge: BtcTxData::new(serialize_hex(bitvm2_graph.challenge.tx())), - disprove: Default::default(), - kickoff: BtcTxData::new(serialize_hex(bitvm2_graph.kickoff.tx())), - pegin: BtcTxData::new(serialize_hex(bitvm2_graph.pegin.tx())), - take1: BtcTxData::new(serialize_hex(bitvm2_graph.take1.tx())), - take2: BtcTxData::new(serialize_hex(bitvm2_graph.take2.tx())), + operator_assert: BtcTxData::new(serialize_hex(bitvm_graph.operator_assert.tx())) + .with_progresses(assert_progresses) + .with_fail_reason(assert_fail_reason), + verifier_asserts: bitvm_graph + .verifier_asserts + .iter() + .map(|tx| BtcTxData::new(serialize_hex(tx.tx()))) + .collect(), + disproves: bitvm_graph + .disproves + .iter() + .map(|tx| BtcTxData::new(serialize_hex(tx.tx()))) + .collect(), + take2: BtcTxData::new(serialize_hex(bitvm_graph.take2.tx())), }; if let Some(challenge_txid) = graph.challenge_txid @@ -1641,11 +1445,6 @@ pub async fn get_graph_txn( { resp.challenge.raw_data = serialize_hex(&tx); } - if let Some(disprove_txid) = graph.disprove_txid - && let Ok(Some(tx)) = app_state.btc_client.get_tx(&disprove_txid.0).await - { - resp.disprove.raw_data = serialize_hex(&tx); - } ok_response(resp) } else { @@ -1670,7 +1469,7 @@ pub async fn get_graph_txn( /// /// # Path Parameters /// -/// - `graph_id`: UUID of the current BitVM2 graph +/// - `graph_id`: UUID of the current BitVM graph /// /// # Returns /// @@ -1727,7 +1526,7 @@ pub async fn get_graph_neighbor_ids( /// /// # Path Parameters /// -/// - `instance_id`: UUID of the BitVM2 instance +/// - `instance_id`: UUID of the BitVM instance /// /// # Returns /// @@ -1794,12 +1593,12 @@ pub async fn get_unsigned_pegin_txn( /// Send challenge transaction for a graph /// -/// Loads the graph from DB, rebuilds the full BitVM2 graph, and broadcasts +/// Loads the graph from DB, rebuilds the full BitVM graph, and broadcasts /// the Challenge transaction on Bitcoin. /// /// # Path Parameters /// -/// - `graph_id`: UUID of the BitVM2 graph to challenge +/// - `graph_id`: UUID of the BitVM graph to challenge /// /// # Returns /// @@ -1831,15 +1630,15 @@ pub async fn send_challenge( ) })?; - let simplified_bitvm2_graph: SimplifiedBitvm2Graph = + let simplified_bitvm_graph: SimplifiedBitvmGcGraph = parse_graph_raw_data(graph_raw_data.raw_data, graph_id_uuid) .await .api_error("SEND_CHALLENGE_ERROR")?; - let bitvm2_graph: Bitvm2Graph = - Bitvm2Graph::from_simplified(&simplified_bitvm2_graph).api_error("SEND_CHALLENGE_ERROR")?; + let bitvm_graph: BitvmGcGraph = + BitvmGcGraph::from_simplified(&simplified_bitvm_graph).api_error("SEND_CHALLENGE_ERROR")?; - let txid = send_challenge_tx(&app_state.btc_client, &bitvm2_graph) + let txid = send_challenge_tx(&app_state.btc_client, &bitvm_graph) .await .api_error("SEND_CHALLENGE_ERROR")?; diff --git a/node/src/rpc_service/handler/node_handler.rs b/node/src/rpc_service/handler/node_handler.rs index 7f1473b3..40d7fb30 100644 --- a/node/src/rpc_service/handler/node_handler.rs +++ b/node/src/rpc_service/handler/node_handler.rs @@ -7,13 +7,13 @@ use crate::rpc_service::validation::InputValidator; use crate::rpc_service::{AppState, current_time_secs}; use crate::utils::reflect_goat_address; use axum::extract::{Path, Query, State}; -use bitvm2_lib::actors::Actor; +use bitvm_lib::actors::Actor; use std::sync::Arc; use store::localdb::NodeQuery; /// Get node list /// -/// Returns a paginated list of BitVM2 network nodes based on query parameters. Supports filtering by +/// Returns a paginated list of BitVM network nodes based on query parameters. Supports filtering by /// GOAT address, actor type, and online status. Automatically updates the current node's timestamp. /// /// # Query Parameters @@ -32,7 +32,7 @@ use store::localdb::NodeQuery; /// /// # Use Case /// -/// Frontend applications use this to display the list of active nodes in the BitVM2 network, +/// Frontend applications use this to display the list of active nodes in the BitVM network, /// showing their roles, availability, and connection information. /// /// # Example @@ -113,7 +113,7 @@ pub async fn get_nodes( /// Get nodes overview statistics /// -/// Returns statistical overview of all nodes in the BitVM2 network, including counts by actor type +/// Returns statistical overview of all nodes in the BitVM network, including counts by actor type /// and online/offline status. Automatically updates the current node's timestamp. /// /// # Returns @@ -140,8 +140,8 @@ pub async fn get_nodes( /// "total": 18, /// "online_operators": 8, /// "offline_operators": 2, -/// "online_challengers": 3, -/// "offline_challengers": 1, +/// "online_verifiers": 3, +/// "offline_verifiers": 1, /// "online_committees": 2, /// "offline_committees": 0, /// "online_watchtowers": 2, diff --git a/node/src/rpc_service/handler/proof_handler.rs b/node/src/rpc_service/handler/proof_handler.rs index 5d0d3c62..c5ed3d7e 100644 --- a/node/src/rpc_service/handler/proof_handler.rs +++ b/node/src/rpc_service/handler/proof_handler.rs @@ -132,7 +132,7 @@ async fn handle_proof_desc_forwarding( /// # Use Case /// /// Applications use this to retrieve proof generation status and metadata for different chain proof types -/// in the BitVM2 network. The endpoint supports forwarding requests to dedicated proof builder services +/// in the BitVM network. The endpoint supports forwarding requests to dedicated proof builder services /// when configured via environment variables. /// /// # Example @@ -195,7 +195,7 @@ pub async fn get_chain_proof_desc( /// /// # Query Parameters /// -/// - `instance_id`: Instance ID (required) - the identifier of the BitVM2 instance +/// - `instance_id`: Instance ID (required) - the identifier of the BitVM instance /// - `graph_id`: Graph ID (required) - the identifier of the graph within the instance /// /// # Returns @@ -213,7 +213,7 @@ pub async fn get_chain_proof_desc( /// # Use Case /// /// Applications use this to retrieve proof generation status and metadata for operator proofs -/// in the BitVM2 network. The endpoint supports forwarding requests to dedicated proof builder services +/// in the BitVM network. The endpoint supports forwarding requests to dedicated proof builder services /// when configured via environment variables. /// /// # Example diff --git a/node/src/rpc_service/mod.rs b/node/src/rpc_service/mod.rs index b2a67541..8e8d5208 100644 --- a/node/src/rpc_service/mod.rs +++ b/node/src/rpc_service/mod.rs @@ -1,5 +1,5 @@ pub mod auth; -mod bitvm2; +mod bitvm; mod cors_config; pub mod handler; mod node; @@ -27,7 +27,7 @@ use axum::{ Router, middleware, routing::{get, post}, }; -use bitvm2_lib::actors::Actor; +use bitvm_lib::actors::Actor; use client::btc_chain::BTCClient; use client::goat_chain::GOATClient; use client::http_client::async_client::HttpAsyncClient; @@ -93,6 +93,30 @@ impl AppState { http_client, })) } + + pub async fn create_arc_mock_app_state( + local_db: LocalDB, + actor: Actor, + peer_id: String, + registry: Arc>, + ) -> anyhow::Result> { + let (btc_client, btc_mock_adaptor) = BTCClient::new_mock_client(); + btc_mock_adaptor.set_height(900_000); + + let (goat_client, goat_mock_adaptor) = GOATClient::new_mock_client(); + goat_mock_adaptor.set_latest_block_number(1_000_000); + goat_mock_adaptor.set_finalized_block_number(999_990); + + Ok(Arc::new(AppState { + local_db, + btc_client, + goat_client, + metrics_state: MetricsState::new(registry), + actor, + peer_id, + http_client: HttpAsyncClient::new(None), + })) + } } /// Root path handler @@ -115,15 +139,11 @@ async fn root() -> &'static str { "Hello, World!" } -pub async fn serve( +pub async fn serve_with_app_state( addr: String, - local_db: LocalDB, - actor: Actor, - peer_id: String, - registry: Arc>, + app_state: Arc, cancellation_token: CancellationToken, ) -> anyhow::Result { - let app_state = AppState::create_arc_app_state(local_db, actor, peer_id, registry).await?; let server = Router::new() .route(routes::ROOT, get(root)) .route(routes::v1::NODES_BASE, get(get_nodes)) @@ -201,6 +221,18 @@ pub async fn serve( } } +pub async fn serve( + addr: String, + local_db: LocalDB, + actor: Actor, + peer_id: String, + registry: Arc>, + cancellation_token: CancellationToken, +) -> anyhow::Result { + let app_state = AppState::create_arc_app_state(local_db, actor, peer_id, registry).await?; + serve_with_app_state(addr, app_state, cancellation_token).await +} + /// This method introduces performance overhead and is temporarily used for debugging with the frontend. /// It will be removed afterwards. async fn print_req_and_resp_detail( @@ -239,7 +271,7 @@ mod tests { use crate::env::{ ENV_GOAT_CHAIN_URL, ENV_GOAT_GATEWAY_CONTRACT_ADDRESS, ENV_PROOF_SEVER_URL, get_network, }; - use crate::rpc_service::bitvm2::{ + use crate::rpc_service::bitvm::{ BRIDGE_IN_AMOUNTS, GraphGetResponse, GraphListResponse, InstanceGetResponse, InstanceListResponse, InstanceOverviewResponse, InstanceSettingResponse, }; @@ -389,7 +421,7 @@ mod tests { let mut nodes = Vec::::new(); let (_, public_key) = Secp256k1::new().generate_keypair(&mut rand::thread_rng()); let pub_key = public_key.to_string(); - let actor = Actor::Challenger; + let actor = Actor::Verifier; nodes.push(Node { peer_id: generate_local_key().public().to_peer_id().to_string(), actor: actor.to_string(), @@ -427,7 +459,7 @@ mod tests { tokio::spawn(rpc_service::serve( addr.clone(), local_db, - Actor::Challenger, + Actor::Verifier, generate_local_key().public().to_peer_id().to_string(), Arc::new(Mutex::new(Registry::default())), CancellationToken::new(), @@ -456,7 +488,7 @@ mod tests { resp_validation: Some(Box::new(|text| -> bool { matches!( serde_json::from_str::(&text), - Ok(node_overview) if node_overview.nodes_overview.online_challengers == 1 && + Ok(node_overview) if node_overview.nodes_overview.online_verifiers == 1 && node_overview.nodes_overview.online_committees == 1 ) })), @@ -467,10 +499,10 @@ mod tests { } #[tokio::test(flavor = "multi_thread")] - async fn test_bitvm2_api() -> Result<(), Box> { + async fn test_bitvm_api() -> Result<(), Box> { init(None); let addr = available_addr(); - let actor = Actor::Challenger; + let actor = Actor::Verifier; let local_key = generate_local_key(); let peer_id = local_key.public().to_peer_id().to_string(); let local_db = create_local_db(&temp_sqlite_db_path()).await; @@ -572,13 +604,13 @@ mod tests { take1_txid: None, challenge_txid: None, take2_txid: None, - disprove_txid: None, - watchtower_challenge_init_txid: None, + operator_assert_txid: None, + verifier_assert_txids: vec![], + disprove_txids: vec![], watchtower_challenge_timeout_txids: vec![], - nack_txids: vec![], - blockhash_commit_timeout_txid: None, - assert_init_txid: None, - assert_commit_timeout_txids: vec![], + operator_challenge_nack_txids: vec![], + operator_commit_timeout_txid: None, + watchtower_challenge_init_txid: None, init_withdraw_tx_hash: Some(format!("0x{}", hex::encode(generate_random_bytes(32)))), bridge_out_start_at: current_time_secs() + 100, status_updated_at: current_time_secs(), @@ -607,13 +639,13 @@ mod tests { take1_txid: None, challenge_txid: None, take2_txid: None, - disprove_txid: None, - watchtower_challenge_init_txid: None, + operator_assert_txid: None, + verifier_assert_txids: vec![], + disprove_txids: vec![], watchtower_challenge_timeout_txids: vec![], - nack_txids: vec![], - blockhash_commit_timeout_txid: None, - assert_init_txid: None, - assert_commit_timeout_txids: vec![], + operator_challenge_nack_txids: vec![], + operator_commit_timeout_txid: None, + watchtower_challenge_init_txid: None, init_withdraw_tx_hash: None, bridge_out_start_at: 0, status_updated_at: current_time_secs(), @@ -765,7 +797,7 @@ mod tests { })), }, ]; - do_batch_tests("bitvm2 apis", &Client::new(), &api_test_items).await?; + do_batch_tests("bitvm apis", &Client::new(), &api_test_items).await?; Ok(()) } diff --git a/node/src/rpc_service/node.rs b/node/src/rpc_service/node.rs index 30982ef1..7e1748a4 100644 --- a/node/src/rpc_service/node.rs +++ b/node/src/rpc_service/node.rs @@ -58,6 +58,7 @@ pub struct NodeDesc { pub trait ToNodeDesc { fn to_node_desc(self, time_threshold: i64, current_peer_id: &str) -> NodeDesc; } + impl ToNodeDesc for Node { fn to_node_desc(self, time_threshold: i64, current_peer_id: &str) -> NodeDesc { let status = if self.updated_at >= time_threshold || self.peer_id == current_peer_id { diff --git a/node/src/rpc_service/routes.rs b/node/src/rpc_service/routes.rs index a6463f5f..901b4bda 100644 --- a/node/src/rpc_service/routes.rs +++ b/node/src/rpc_service/routes.rs @@ -25,8 +25,11 @@ pub(crate) mod v1 { // pub const PROOFS_BASE: &str = "/v1/proofs"; pub const PROOFS_CHAIN_PROOFS_DESC: &str = "/v1/proofs/chain_proofs_desc"; pub const NODES_WATCHTOWER_BASE: &str = "/v1/proofs/watchtower_proofs"; + #[allow(dead_code)] pub const NODES_OPERATOR_BASE: &str = "/v1/proofs/operator_proofs"; + pub const PROOFS_WRAPPER_PROOF: &str = "/v1/proofs/wrapper_proofs"; pub const PROOFS_OPERATOR_PROOF_DESC: &str = "/v1/proofs/operator_proofs_desc"; pub const PROOFS_WATCHTOWER_PROOF_TIMEOUT: &str = "/v1/proofs/watchtower_proofs_timeout"; + #[allow(dead_code)] pub const PROOFS_OPERATOR_PROOF_TIMEOUT: &str = "/v1/proofs/operator_proofs_timeout"; } diff --git a/node/src/rpc_service/validation.rs b/node/src/rpc_service/validation.rs index d662dc65..ee284346 100644 --- a/node/src/rpc_service/validation.rs +++ b/node/src/rpc_service/validation.rs @@ -5,7 +5,7 @@ use alloy::primitives::Address as EvmAddress; use axum::Json; use axum::http::StatusCode; use bitcoin::{Address, AddressType, Network, PublicKey}; -use bitvm2_lib::actors::Actor; +use bitvm_lib::actors::Actor; use libp2p::PeerId; use std::str::FromStr; use uuid::Uuid; @@ -182,8 +182,10 @@ impl InputValidator { Json(ErrorResponse { error: "INVALID_TX_NAME_TYPE".to_string(), message: format!( - "Invalid tx_name: {tx_name}. Valid values are: pegin.hex, kickoff.hex, pre-kickoff.hex, \ - watchtower-challenge-init.hex, assert-init.hex, challenge.hex, take1.hex, take2.hex, disprove.hex" + "Invalid tx_name: {tx_name}. Valid values are: cur-pre-kickoff.hex, next-pre-kickoff.hex, \ + force-skip-kickoff.hex, quick-challenge.hex, challenge-incomplete-kickoff.hex, pegin.hex, \ + kickoff.hex, take1.hex, challenge.hex, watchtower-challenge-init.hex, operator-assert.hex, \ + verifier-assert.hex, disprove.hex, take2.hex. Use index for verifier-assert.hex and disprove.hex." ), }), )), diff --git a/node/src/scheduled_tasks/event_watch_task.rs b/node/src/scheduled_tasks/event_watch_task.rs index ed58abe7..ca8c344e 100644 --- a/node/src/scheduled_tasks/event_watch_task.rs +++ b/node/src/scheduled_tasks/event_watch_task.rs @@ -19,8 +19,8 @@ use alloy::sol_types::{SolType, SolValue}; use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::Hash; use bitcoin::{Address, Amount, OutPoint, Txid}; -use bitvm2_lib::actors::Actor; -use bitvm2_lib::types::UserInfo; +use bitvm_lib::actors::Actor; +use bitvm_lib::types::UserInfo; use client::btc_chain::BTCClient; use client::goat_chain::{GOATClient, GoatInitConfig}; use client::graphs::GraphQueryClient; @@ -454,10 +454,10 @@ async fn handle_withdraw_disproved_events<'a>( ) -> anyhow::Result<()> { for event in withdraw_disproved_events { let graph_id = Uuid::from_str(&strip_hex_prefix_owned(&event.graph_id))?; - let (flag, challenger_addr) = reflect_goat_address(Some(event.challenger_addr.clone())); + let (flag, verifier_addr) = reflect_goat_address(Some(event.challenger_addr.clone())); if !flag { warn!( - "handle_withdraw_disproved_events failed as cast challenger address failed, detail: {}, {}", + "handle_withdraw_disproved_events failed as cast verifier address failed, detail: {}, {}", event.transaction_hash, event.challenger_addr ); continue; @@ -473,7 +473,7 @@ async fn handle_withdraw_disproved_events<'a>( add_node_reward( storage_processor, - &challenger_addr.unwrap(), + &verifier_addr.unwrap(), U256::from_str(&event.challenger_amount_sats).unwrap_or_default(), ) .await?; @@ -1230,7 +1230,25 @@ pub async fn run_watch_event_task( })], ), ( - Actor::Challenger, + Actor::Verifier, + vec![WatchEventConfig::Gateway(TheGraphConfig { + address: gateway_contract, + the_graph_url: get_goat_gateway_the_graph_urls_from_env(), + event_entities: vec![ + GatewayEventEntity::InitWithdraws, + GatewayEventEntity::CancelWithdraws, + GatewayEventEntity::ProceedWithdraws, + GatewayEventEntity::WithdrawHappyPaths, + GatewayEventEntity::WithdrawUnhappyPaths, + GatewayEventEntity::WithdrawDisproveds, + GatewayEventEntity::BridgeInRequests, + GatewayEventEntity::BridgeIns, + GatewayEventEntity::PostGraphDatas, + ], + })], + ), + ( + Actor::Verifier, vec![WatchEventConfig::Gateway(TheGraphConfig { address: gateway_contract, the_graph_url: get_goat_gateway_the_graph_urls_from_env(), diff --git a/node/src/scheduled_tasks/graph_maintenance_tasks.rs b/node/src/scheduled_tasks/graph_maintenance_tasks.rs index f9838e26..dfed5b02 100644 --- a/node/src/scheduled_tasks/graph_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/graph_maintenance_tasks.rs @@ -1,25 +1,24 @@ use crate::action::{ - AssertCommitTimeout, AssertInitReady, ChallengeSent, DisproveReady, DisproveSent, - GOATMessageContent, KickoffReady, KickoffSent, OperatorAckTimeout, - OperatorCommitBlockHashReady, OperatorCommitBlockHashTimeout, PreKickoffSent, Take1Ready, - Take1Sent, Take2Ready, Take2Sent, WatchtowerChallengeInitSent, WatchtowerChallengeSent, - WatchtowerChallengeTimeout, + AssertReady, AssertSent, ChallengeAssertSent, ChallengeSent, DisproveSent, GOATMessageContent, + KickoffReady, KickoffSent, NackReady, OperatorCommitPubinReady, OperatorCommitPubinTimeout, + PreKickoffSent, Take1Ready, Take1Sent, Take2Ready, Take2Sent, WatchtowerChallengeInitSent, + WatchtowerChallengeSent, WatchtowerChallengeTimeout, WronglyChallengeTimeout, }; use crate::env::get_network; use crate::rpc_service::current_time_secs; use crate::scheduled_tasks::fetch_on_turn_graph_by_status; use crate::utils::{SELF_SENDER, outpoint_spent_txid, upsert_message}; use bitcoin::Txid; -use bitvm2_lib::actors::Actor; -use bitvm2_lib::challenger::{ - assert_commit_timeout_timelock, commit_blockhash_timeout_timelock, nack_timelock, -}; -use bitvm2_lib::operator::{ - take1_timelock, take2_timelocks, watchtower_challenge_timeout_timelock, -}; +use bitvm_lib::actors::Actor; +use bitvm_lib::operator::{take1_timelock, take2_timelock}; +use bitvm_lib::verifier::disprove_timelock; use client::btc_chain::BTCClient; use client::goat_chain::DisproveTxType; -use indexmap::IndexMap; +use goat::{ + constants::{OPERATOR_ACK_TIMELOCK, OPERATOR_COMMIT_TIMELOCK, WATCHTOWER_CHALLENGE_TIMELOCK}, + transactions::base::output_topology, + utils::num_blocks_per_network, +}; use serde::{Deserialize, Serialize}; use store::localdb::{LocalDB, StorageProcessor}; use store::{ @@ -29,422 +28,151 @@ use strum::{Display, EnumString}; use tracing::{info, trace, warn}; use uuid::Uuid; -const CONNECTOR_G_MARGIN: i64 = 3; -// const CONNECTOR_D_MARGIN: u64 = 2; -// const CONNECTOR_F_MARGIN: u64 = 2; -const CONNECTOR_GUARDIAN_MARGIN: u64 = 2; - const MONITE_BTC_TX_NAME_KICKOFF: &str = "kickoff"; const MONITE_BTC_TX_NAME_WATCHTOWER_INIT: &str = "watchtower_init"; -const MONITE_BTC_TX_NAME_ASSERT_INIT: &str = "assert_init"; - -#[derive(Clone, Debug)] -pub struct ChallengeTimeLockConfig { - pub watchtower_challenge_timelock: i64, - pub watchtower_ack_timelock: i64, - pub watchtower_blockhash_commit_timelock: i64, - pub assert_commit_timelock: i64, -} - -fn get_challenge_timelock_config() -> ChallengeTimeLockConfig { - ChallengeTimeLockConfig { - watchtower_challenge_timelock: watchtower_challenge_timeout_timelock(get_network()) as i64, - watchtower_ack_timelock: nack_timelock(get_network()) as i64, - watchtower_blockhash_commit_timelock: commit_blockhash_timeout_timelock(get_network()) - as i64, - assert_commit_timelock: assert_commit_timeout_timelock(get_network()) as i64, - } -} +const MONITE_BTC_TX_NAME_PROVER_ASSERT: &str = "prover_assert"; +const MONITE_BTC_TX_NAME_VERIFIER_ASSERT: &str = "verifier_assert"; fn get_take1_timelock_config() -> i64 { take1_timelock(get_network()) as i64 } -pub struct Take2TimeLockConfig { - pub assert_init_out_timelock: i64, - pub watchtower_challenge_init_out_timelock: i64, +fn get_take2_timelock_config() -> i64 { + take2_timelock(get_network()) as i64 } -fn get_take2_timelock_config() -> Take2TimeLockConfig { - let (watchtower_challenge_init_out_timelock, assert_init_out_timelock) = - take2_timelocks(get_network()); - Take2TimeLockConfig { - assert_init_out_timelock: assert_init_out_timelock as i64, - watchtower_challenge_init_out_timelock: watchtower_challenge_init_out_timelock as i64, - } + +fn get_disprove_timelock_config() -> i64 { + disprove_timelock(get_network()) as i64 } + #[derive(Clone, Debug, Eq, PartialEq, Display, EnumString)] enum OperatorWithdrawType { Take1, Take2, } -/// Watchtower init tx vout item status watchtower processed -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq, Display, EnumString, -)] -pub enum CommitBlockHashStatus { - #[default] - None, - WatchtowerChallengeProcessed, // watchtower challenge processed, but commit blockhash not detected yet - OperatorCommit, // commit blockhash detected - OperatorCommitTimeout, // commit blockhash not detected, but timelock expired -} - -#[derive( - Copy, Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq, Display, EnumString, -)] -pub enum AssertCommitStatus { - #[default] - None, - OperatorInit, // assert init vout detected, but some assert commit not detected yet - OperatorCommit, // all assert commit sent - OperatorCommitTimeout, // some assert commit not sent, but timelock expired -} - #[derive( Copy, Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq, Display, EnumString, )] -pub enum WatchtowerChallengeStatus { +pub enum VerifierChallengeStatus { #[default] None, - OperatorInit, // watchtower init detected, but no challenge detected yet - WatchtowerChallenge, // Some Watchtower challenge, and timelock not expired - WatchtowerChallengeTimeout, // Some Watchtower did not challenge, and timelock expired - OperatorACKTimeout, // Operator did not send ACK for some Watchtower, and timelock expired - WatchtowerChallengeNormalFinished, // Normal Finished, TODO: rename it - WatchtowerChallengeDisproveFinished, // Disproved Finished + VerifierAsserted, + ProverAnswered, + Disproved, } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Default)] pub struct ChallengeSubStatus { - pub watchtower_challenge_status: WatchtowerChallengeStatus, - pub commit_blockhash_status: CommitBlockHashStatus, - pub assert_commit_status: AssertCommitStatus, + pub watchtower_challenge_status: Vec, // true for challenge connector spend + pub verifier_challenge_status: Vec, pub disprove_type: Option, pub disprove_index: i32, } -impl ChallengeSubStatus { - pub fn is_watchtower_challenge_success(&self) -> bool { - self.watchtower_challenge_status - == WatchtowerChallengeStatus::WatchtowerChallengeNormalFinished - && self.commit_blockhash_status == CommitBlockHashStatus::OperatorCommit - } - - pub fn is_disproved(&self) -> bool { - self.disprove_type.is_some() - } - - pub fn is_all_commit_success(&self) -> bool { - self.is_watchtower_challenge_success() && self.is_assert_commit_success() - } - - pub fn is_assert_commit_success(&self) -> bool { - self.assert_commit_status == AssertCommitStatus::OperatorCommit - } -} - -#[derive(Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq, Display, EnumString)] -pub enum WatchtowerChallengeItemStatus { - #[default] - None, - OperatorInit, - Challenge, - ChallengeTimeout, - OperatorACK, - OperatorNACK, -} - -/// Watchtower init tx vout data -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct WTInitTxVoutMonitorData { - pub data_map: IndexMap, - pub require_disproved_indexes: Vec, - pub commit_blockhash_status: CommitBlockHashStatus, +struct DetectedGraphMessage { + actor: Actor, + content: GOATMessageContent, + sub_type: Option, } -impl WTInitTxVoutMonitorData { - pub fn new(index_size: i32) -> Self { - let mut data_map: IndexMap = IndexMap::new(); - for i in 0..index_size { - data_map.insert(i, WatchtowerChallengeItemStatus::OperatorInit); - } - Self { - data_map, - require_disproved_indexes: vec![], - commit_blockhash_status: CommitBlockHashStatus::None, - } - } - pub async fn monitor_vout( - &mut self, - btc_client: &BTCClient, - txid: &Txid, - input_challenge_timeout_txids: &[SerializableTxid], - nack_txids: &[SerializableTxid], - commit_blockhash_timeout_txid: &SerializableTxid, - ) -> anyhow::Result<(Vec<(usize, Txid)>, Vec<(usize, Txid)>, Vec<(usize, Txid)>)> { - let mut challenge_txids: Vec<(usize, Txid)> = Vec::new(); - let mut challenge_timeout_txids: Vec<(usize, Txid)> = Vec::new(); - let mut ack_txids: Vec<(usize, Txid)> = Vec::new(); - for (k, status) in self.data_map.iter_mut() { - let index = *k; - if *status == WatchtowerChallengeItemStatus::OperatorInit - && let Some(spend_txid) = - outpoint_spent_txid(btc_client, txid, (index * 2) as u64).await? - { - if input_challenge_timeout_txids.iter().any(|v| v.0 == spend_txid) { - *status = WatchtowerChallengeItemStatus::ChallengeTimeout; - challenge_timeout_txids.push((index as usize, spend_txid)); - } else { - *status = WatchtowerChallengeItemStatus::Challenge; - challenge_txids.push((index as usize, spend_txid)); - } - } - - if *status == WatchtowerChallengeItemStatus::Challenge - && let Some(spend_txid) = - outpoint_spent_txid(btc_client, txid, (index * 2 + 1) as u64).await? - { - if nack_txids.iter().any(|v| v.0 == spend_txid) { - *status = WatchtowerChallengeItemStatus::OperatorNACK; - } else { - *status = WatchtowerChallengeItemStatus::OperatorACK; - ack_txids.push((index as usize, spend_txid)); - } - } - } - let connector_g_vout = self.data_map.len() as i32 * 2; - if matches!( - self.commit_blockhash_status, - CommitBlockHashStatus::None | CommitBlockHashStatus::WatchtowerChallengeProcessed - ) && let Some(spend_txid) = - outpoint_spent_txid(btc_client, txid, connector_g_vout as u64).await? - { - if commit_blockhash_timeout_txid.0 == spend_txid { - self.commit_blockhash_status = CommitBlockHashStatus::OperatorCommitTimeout; - } else { - self.commit_blockhash_status = CommitBlockHashStatus::OperatorCommit; - } - } else if self.commit_blockhash_status == CommitBlockHashStatus::None - && self.is_commit_blockhash_ready() - { - self.commit_blockhash_status = CommitBlockHashStatus::WatchtowerChallengeProcessed; - } - Ok((challenge_txids, challenge_timeout_txids, ack_txids)) - } - - fn update_disprove_indexes(&mut self) { - self.require_disproved_indexes = vec![]; - for (index, status) in self.data_map.iter() { - if matches!( - *status, - WatchtowerChallengeItemStatus::OperatorInit - | WatchtowerChallengeItemStatus::Challenge - ) { - self.require_disproved_indexes.push(*index as usize); - } - } - } - - fn get_require_disproved_string(&self) -> String { - format!( - "[{}]", - self.require_disproved_indexes - .iter() - .map(|v| v.to_string()) - .collect::>() - .join("_") - ) - } - - pub fn get_challenge_process_desc(&self) -> (usize, usize) { - ( - self.data_map - .iter() - .filter(|(_, v)| { - matches!( - **v, - WatchtowerChallengeItemStatus::Challenge - | WatchtowerChallengeItemStatus::OperatorACK - ) - }) - .count(), - self.data_map.len(), - ) - } - - pub fn get_challenge_timeout_process_desc(&self) -> (usize, usize) { - ( - self.data_map - .iter() - .filter(|(_, v)| **v == WatchtowerChallengeItemStatus::ChallengeTimeout) - .count(), - self.data_map.len(), - ) - } - - pub fn get_commit_block_hash_desc(&self) -> (usize, usize) { - match self.commit_blockhash_status { - CommitBlockHashStatus::OperatorCommit => (1, 1), - _ => (0, 1), - } - } - - pub fn get_commit_block_hash_timeout_desc(&self) -> (usize, usize) { - match self.commit_blockhash_status { - CommitBlockHashStatus::OperatorCommitTimeout => (1, 1), - _ => (0, 1), - } - } - - pub fn get_ack_process_desc(&self) -> (usize, usize) { - ( - self.data_map - .iter() - .filter(|(_, v)| **v == WatchtowerChallengeItemStatus::OperatorACK) - .count(), - self.data_map - .iter() - .filter(|(_, v)| { - matches!( - **v, - WatchtowerChallengeItemStatus::Challenge - | WatchtowerChallengeItemStatus::OperatorACK - | WatchtowerChallengeItemStatus::OperatorNACK - ) - }) - .count(), - ) - } - - pub fn is_watchtower_challenge_success(&self) -> bool { - self.data_map.values().all(|status| { - matches!( - status, - WatchtowerChallengeItemStatus::OperatorACK - | WatchtowerChallengeItemStatus::ChallengeTimeout - ) - }) && self.commit_blockhash_status == CommitBlockHashStatus::OperatorCommit +impl DetectedGraphMessage { + fn new(actor: Actor, content: GOATMessageContent) -> Self { + Self { actor, content, sub_type: None } } - pub fn is_commit_blockhash_processed(&self) -> bool { - matches!( - self.commit_blockhash_status, - CommitBlockHashStatus::OperatorCommit | CommitBlockHashStatus::OperatorCommitTimeout - ) + fn with_sub_type(actor: Actor, content: GOATMessageContent, sub_type: String) -> Self { + Self { actor, content, sub_type: Some(sub_type) } } +} - pub fn is_commit_blockhash_ready(&self) -> bool { - self.data_map.values().all(|status| { - matches!( - status, - WatchtowerChallengeItemStatus::OperatorACK - | WatchtowerChallengeItemStatus::ChallengeTimeout - | WatchtowerChallengeItemStatus::Challenge - ) - }) +impl ChallengeSubStatus { + pub fn is_watchtower_challenge_success(&self, required_watchtower_num: usize) -> bool { + self.watchtower_challenge_status + .iter() + .filter(|&&status| status) + .take(required_watchtower_num) + .count() + == required_watchtower_num } pub fn is_disproved(&self) -> bool { - self.data_map.values().any(|status| *status == WatchtowerChallengeItemStatus::OperatorNACK) - || self.commit_blockhash_status == CommitBlockHashStatus::OperatorCommitTimeout + self.disprove_type.is_some() } } -/// Assert init tx vout item status -#[derive(Clone, Debug, Serialize, Deserialize, Default, Eq, PartialEq, Display, EnumString)] -pub enum AssertCommitItemStatus { - #[default] - None, - OperatorInit, - OperatorCommit, - OperatorCommitTimeout, -} -/// Assert init tx vout data -#[derive(Clone, Debug, Serialize, Deserialize)] -pub struct AssertInitTxVoutMonitorData { - pub data_map: IndexMap, - pub require_disproved_indexes: Vec, -} - -impl AssertInitTxVoutMonitorData { - pub fn new(index_size: i32) -> Self { - let mut data_map: IndexMap = IndexMap::new(); - for i in 0..index_size { - data_map.insert(i, AssertCommitItemStatus::OperatorInit); - } - Self { data_map, require_disproved_indexes: vec![] } - } - pub async fn monitor_vout( - &mut self, - btc_client: &BTCClient, - txid: &Txid, - committ_timeout_txids: &[SerializableTxid], - ) -> anyhow::Result { - let mut vout_spent_detect = 0; - for (k, status) in self.data_map.iter_mut() { - if *status == AssertCommitItemStatus::OperatorInit - && let Some(spend_txid) = outpoint_spent_txid(btc_client, txid, *k as u64).await? - { - if committ_timeout_txids.iter().any(|v| v.0 == spend_txid) { - *status = AssertCommitItemStatus::OperatorCommitTimeout; - } else { - *status = AssertCommitItemStatus::OperatorCommit; - } - vout_spent_detect += 1 - } - } - Ok(vout_spent_detect) - } - - pub fn is_assert_success(&self) -> bool { - self.data_map.values().all(|status| *status == AssertCommitItemStatus::OperatorCommit) +async fn upsert_detected_messages( + local_db: &LocalDB, + graph_id: Uuid, + messages: Vec, +) -> anyhow::Result<()> { + if messages.is_empty() { + return Ok(()); } - pub fn get_commit_process_desc(&self) -> (usize, usize) { - ( - self.data_map - .iter() - .filter(|(_, v)| **v == AssertCommitItemStatus::OperatorCommit) - .count(), - self.data_map.len(), + let mut storage_processor = local_db.acquire().await?; + for message in messages { + upsert_message( + &mut storage_processor, + false, + graph_id, + message.sub_type, + SELF_SENDER.to_string(), + message.actor, + message.content, + 0, + 0, ) + .await?; } + Ok(()) +} - fn update_disprove_indexes(&mut self) { - self.require_disproved_indexes = vec![]; - for (index, status) in self.data_map.iter() { - if *status == AssertCommitItemStatus::OperatorInit { - self.require_disproved_indexes.push(*index as usize); - } - } +async fn get_confirmed_tx_monitor( + btc_client: &BTCClient, + local_db: &LocalDB, + graph_id: Uuid, + txid: Txid, + tx_name: String, +) -> anyhow::Result> { + let txid_serial = SerializableTxid::from(txid); + let existing = { + let mut storage_processor = local_db.acquire().await?; + storage_processor.find_graph_btc_tx_vout_monitor(&graph_id, &txid_serial).await? + }; + if let Some(existing) = existing + && existing.height > 0 + && existing.vout_len > 0 + { + return Ok(Some(existing)); } - fn get_require_disproved_string(&self) -> String { - format!( - "[{}]", - self.require_disproved_indexes - .iter() - .map(|v| v.to_string()) - .collect::>() - .join("_") - ) + let Some(tx_info) = btc_client.get_tx_info(&txid_serial.0).await? else { + return Ok(None); + }; + let height = tx_info.status.block_height.unwrap_or_default() as i64; + if height <= 0 { + return Ok(None); } - fn is_disproved(&self) -> bool { - self.data_map - .values() - .any(|status| *status == AssertCommitItemStatus::OperatorCommitTimeout) - } + let current_times = current_time_secs(); + let monitor = GraphBtcTxVoutMonitor { + graph_id, + tx_name, + txid: txid_serial, + height, + vout_len: tx_info.vout.len() as i64, + monitor_data: String::new(), + created_at: current_times, + updated_at: current_times, + }; + let mut storage_processor = local_db.acquire().await?; + storage_processor.upsert_graph_btc_tx_vout_monitor(&monitor).await?; + Ok(Some(monitor)) } -/// Parse monitor data from JSON string -fn parse_monitor_data(monitor_data: &str) -> anyhow::Result -where - T: serde::de::DeserializeOwned, -{ - serde_json::from_str(monitor_data) - .map_err(|e| anyhow::anyhow!("Failed to parse monitor data: {e}")) +fn is_timelock_ready(confirmed_height: i64, lock_blocks: i64, current_height: i64) -> bool { + confirmed_height > 0 && confirmed_height + lock_blocks <= current_height } pub async fn get_user_init_withdraw_graphs<'a>( @@ -611,582 +339,604 @@ pub async fn process_graph_challenge( local_db: &LocalDB, btc_client: &BTCClient, ) -> anyhow::Result<()> { + trace!("start tick action: process_graph_challenge"); + let graphs = { let mut storage_processor = local_db.acquire().await?; fetch_on_turn_graph_by_status(&mut storage_processor, &GraphStatus::Challenge.to_string()) .await? }; let current_height = btc_client.get_height().await? as i64; + info!( + "start tick action: process_graph_challenge, graphs: {}, current_height: {current_height}", + graphs.len() + ); + for graph in graphs { - // if detect_kickoff_ref_disprove_tx(btc_client, local_db, &graph).await? { - // warn!( - // "process_graph_challenge detect_kickoff_ref_disprove_tx happened at graph:{}", - // graph.graph_id - // ); - // continue; - // } - let mut sub_status: ChallengeSubStatus = match serde_json::from_str(&graph.sub_status) { - Ok(sub_status) => sub_status, - Err(_) => { - warn!( - "failed to deserialize sub_status at graph:{}, reset to default, old sub_status {}", - graph.graph_id, graph.sub_status - ); - ChallengeSubStatus::default() - } - }; - let mut is_watchtower_challenge_success = sub_status.is_watchtower_challenge_success(); - let mut is_assert_commit_success = sub_status.is_assert_commit_success(); - let mut is_all_commit_success = is_watchtower_challenge_success && is_assert_commit_success; - if !sub_status.is_disproved() && !is_all_commit_success { - trace!("graph:{} is not disproved", graph.graph_id); - if !is_watchtower_challenge_success { - info!("graph:{} watchtower challenge is processing", graph.graph_id); - // process_watchtower_challenge_monitoring may trigger: WatchtowerChallengeSent, WatchtowerChallengeTimeout, OperatorAckTimeout, DisproveSent(OperatorCommitTimeout/OperatorNack), OperatorCommitBlockHashReady, OperatorCommitBlockHashTimeout - is_watchtower_challenge_success = process_watchtower_challenge_monitoring( - btc_client, - local_db, - &graph, - &mut sub_status, - current_height, - ) - .await?; - } - if is_watchtower_challenge_success && !is_assert_commit_success { - info!("graph:{} assert commit is processing", graph.graph_id); - // upsert AssertInitReady message whenever watchtower challenge is finished normally, repeated inserts are idempotent - upsert_message( - &mut local_db.acquire().await?, - false, - graph.graph_id, - None, - SELF_SENDER.to_string(), - Actor::Operator, - GOATMessageContent::AssertInitReady(AssertInitReady { - instance_id: graph.instance_id, - graph_id: graph.graph_id, - }), - 0, - 0, - ) - .await?; - // process_assert_commit_monitoring may trigger: DisproveSent(AssertTimeout), AssertCommitTimeout - is_assert_commit_success = process_assert_commit_monitoring( - btc_client, - local_db, - &graph, - &mut sub_status, - current_height, - ) - .await?; - } - is_all_commit_success = is_watchtower_challenge_success && is_assert_commit_success; + let watchtower_flow_messages = + detect_watchtower_flow(btc_client, local_db, &graph, current_height).await?; + if !watchtower_flow_messages.is_empty() { + info!( + "process_graph_challenge detected {} watchtower/pubin flow messages", + watchtower_flow_messages.len() + ); + upsert_detected_messages(local_db, graph.graph_id, watchtower_flow_messages).await?; + } + + let assert_sent_messages = detect_assert_sent_flow(btc_client, local_db, &graph).await?; + if !assert_sent_messages.is_empty() { + info!( + "process_graph_challenge detected {} assert/challenge-assert messages", + assert_sent_messages.len() + ); + upsert_detected_messages(local_db, graph.graph_id, assert_sent_messages).await?; + } + + if let Some((actor, message_content, sub_type)) = + detect_assert_disprove_ready(btc_client, local_db, &graph, current_height).await? + { + info!("process_graph_challenge detect assert disprove ready"); + let mut storage_processor = local_db.acquire().await?; + upsert_message( + &mut storage_processor, + false, + graph.graph_id, + sub_type, + SELF_SENDER.to_string(), + actor, + message_content, + 0, + 0, + ) + .await?; } - if !sub_status.is_disproved() && is_all_commit_success { + + // take2 monitor + if let Some((actor, message_content)) = + detect_take2(btc_client, local_db, &graph, current_height).await? + { + info!("process_graph_challenge detect take2 ready or take2 sent or disprove sent"); let mut storage_processor = local_db.acquire().await?; - info!("graph:{} watchtower challenge and assert commit is finished", graph.graph_id); - // upsert DisproveReady whenever both watchtower-challenge and assert is finished normally, repeated inserts are idempotent upsert_message( &mut storage_processor, false, graph.graph_id, None, SELF_SENDER.to_string(), - Actor::Challenger, - GOATMessageContent::DisproveReady(DisproveReady { - instance_id: graph.instance_id, - graph_id: graph.graph_id, - }), + actor, + message_content, 0, 0, ) .await?; - // detect_take2 may return: Take2Ready, Take2Sent, DisproveSent(Disprove) - if let Some((actor, message_content)) = - detect_take2(btc_client, local_db, &graph, current_height).await? - { - let mut storage_processor = local_db.acquire().await?; - upsert_message( - &mut storage_processor, - false, - graph.graph_id, - None, - SELF_SENDER.to_string(), - actor, - message_content, - 0, - 0, - ) - .await?; - } } } - Ok(()) } -/// Check if Take1Ready Take2Ready message needs to be sent -async fn check_operator_withdraw_ready_condition( +/// may trigger: +/// - WatchtowerChallengeInitSent +/// - WatchtowerChallengeSent +/// - WatchtowerChallengeTimeout +/// - NackReady +/// - OperatorCommitPubinReady +/// - OperatorCommitPubinTimeout +/// - AssertReady +async fn detect_watchtower_flow( btc_client: &BTCClient, local_db: &LocalDB, - graph_id: Uuid, - check_tx_items: Vec<(Txid, String, OperatorWithdrawType, i64, i64)>, // (txid, tag, height, lock_blocks) + graph: &Graph, current_height: i64, -) -> anyhow::Result { - info!( - "check_operator_withdraw_ready_condition for graph_id: {graph_id}, check tx size: {}, detail:{check_tx_items:?}", - check_tx_items.len() - ); - let mut ready = true; - for (txid, tx_name, operator_withdraw_type, height, lock_blocks) in check_tx_items { - let height = if height <= 0 { - let current_times = current_time_secs(); - let (height, vout_len) = match btc_client.get_tx_info(&txid).await? { - Some(tx_info) => ( - tx_info.status.block_height.unwrap_or_default() as i64, - tx_info.vout.len() as i64, - ), - None => { - info!("graph_id:{graph_id}, {operator_withdraw_type} txid {txid} not on chain",); - return Ok(false); - } - }; - let txid_serial: SerializableTxid = txid.into(); - let mut storage_processor = local_db.acquire().await?; - let existing = - storage_processor.find_graph_btc_tx_vout_monitor(&graph_id, &txid_serial).await?; - let (monitor_data, created_at, tx_name_to_use) = match existing { - Some(existing) => (existing.monitor_data, existing.created_at, existing.tx_name), - None => ("".to_string(), current_times, tx_name.clone()), - }; - storage_processor - .upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { - graph_id, - tx_name: tx_name_to_use, - txid: txid_serial, - height, - vout_len, - monitor_data, - created_at, - updated_at: current_times, - }) - .await?; - height - } else { - height - }; +) -> anyhow::Result> { + let watchtower_challenge_init_txid: Txid = match graph.watchtower_challenge_init_txid.clone() { + Some(txid) => txid.into(), + None => { + warn!( + "detect_watchtower_challenge graph_id:{} watchtower_challenge_init_txid is none", + graph.graph_id + ); + return Ok(vec![]); + } + }; - info!( - "graph_id:{graph_id}, {operator_withdraw_type} txid {txid} at height {height} lock_blocks {lock_blocks}, current height: {current_height} ", + let Some(monitor) = get_confirmed_tx_monitor( + btc_client, + local_db, + graph.graph_id, + watchtower_challenge_init_txid, + MONITE_BTC_TX_NAME_WATCHTOWER_INIT.to_string(), + ) + .await? + else { + trace!( + "detect_watchtower_flow graph_id:{} watchtower challenge init txid {} not confirmed", + graph.graph_id, watchtower_challenge_init_txid ); + return Ok(vec![]); + }; - if height == 0 || height > 0 && height + lock_blocks > current_height { - ready = false; - break; - } - } - Ok(ready) -} + let mut messages = vec![DetectedGraphMessage::new( + Actor::Watchtower, + GOATMessageContent::WatchtowerChallengeInitSent(WatchtowerChallengeInitSent { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + }), + )]; -/// Process graph data in KickOff status -/// may return: Take1Ready, Take1Sent, ChallengeSent -async fn process_kickoff_graph( - btc_client: &BTCClient, - local_db: &LocalDB, - graph: &Graph, - lock_blocks: i64, - current_height: i64, -) -> anyhow::Result> { - trace!("process_kickoff_graph: {}", graph.graph_id); - let (kickoff_txid, take1_txid) = match (graph.kickoff_txid.clone(), graph.take1_txid.clone()) { - (Some(kickoff), Some(take1)) => (kickoff.into(), take1.into()), - _ => { - // NOTE: revert to previous status? - warn!("process_kickoff_graph graph_id:{}, kickoff or take1 is none", graph.graph_id); - return Ok(None); - } - }; - let spent_txid = match outpoint_spent_txid(btc_client, &kickoff_txid, 0).await? { - Some(txid) => txid, - None => { - // kickoff output not spent, check if we need to send Take1Ready - let height = { - let mut storage_processor = local_db.acquire().await?; - storage_processor - .find_graph_btc_tx_vout_monitor(&graph.graph_id, &kickoff_txid.into()) - .await? - .unwrap_or_default() - .height - }; - if check_operator_withdraw_ready_condition( - btc_client, - local_db, - graph.graph_id, - vec![( - kickoff_txid, - MONITE_BTC_TX_NAME_KICKOFF.to_string(), - OperatorWithdrawType::Take1, - height, - lock_blocks, - )], - current_height, - ) - .await? - { - info!( - "process_kickoff_graph graph_id:{}, take1 is ready to send to btc chain", - graph.graph_id - ); - return Ok(Some(( + let watchtower_num = output_topology::watchtower_challenge_init::watchtower_num( + monitor.vout_len.max(0) as usize, + ); + if watchtower_num == 0 { + return Ok(messages); + } + + let connector_e_vout = + output_topology::watchtower_challenge_init::connector_e(watchtower_num) as u64; + let connector_f_vout = + output_topology::watchtower_challenge_init::connector_f(watchtower_num) as u64; + let connector_e_spent_txid = + outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid, connector_e_vout).await?; + let connector_f_spent_txid = + outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid, connector_f_vout).await?; + let operator_commit_timeout_txid: Option = + graph.operator_commit_timeout_txid.clone().map(Into::into); + let operator_commit_timeout_on_chain = + match (connector_e_spent_txid.as_ref(), operator_commit_timeout_txid.as_ref()) { + (Some(spent_txid), Some(timeout_txid)) => spent_txid == timeout_txid, + _ => false, + }; + let pubin_commit_completed = + connector_e_spent_txid.is_some() && !operator_commit_timeout_on_chain; + + let mut all_watchtower_connectors_spent = true; + let mut any_watchtower_timeout_ready = false; + let mut any_nack_ready = false; + for watchtower_index in 0..watchtower_num { + let watchtower_vout = + output_topology::watchtower_challenge_init::watchtower_connector(watchtower_index) + as u64; + let ack_vout = + output_topology::watchtower_challenge_init::ack_connector(watchtower_index) as u64; + let watchtower_spent_txid = + outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid, watchtower_vout) + .await?; + let ack_spent_txid = + outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid, ack_vout).await?; + let timeout_txid = + graph.watchtower_challenge_timeout_txids.get(watchtower_index).cloned().map(Into::into); + let watchtower_timeout_spent = match (watchtower_spent_txid.as_ref(), timeout_txid.as_ref()) + { + (Some(spent_txid), Some(timeout_txid)) => spent_txid == timeout_txid, + _ => false, + }; + + match watchtower_spent_txid { + Some(_) if !watchtower_timeout_spent => { + messages.push(DetectedGraphMessage::with_sub_type( Actor::Operator, - GOATMessageContent::Take1Ready(Take1Ready { + GOATMessageContent::WatchtowerChallengeSent(WatchtowerChallengeSent { instance_id: graph.instance_id, graph_id: graph.graph_id, + watchtower_index, }), - ))); - } else { - trace!("process_kickoff_graph graph_id:{}, take1 not ready", graph.graph_id); + watchtower_index.to_string(), + )); + + if ack_spent_txid.is_none() + && connector_f_spent_txid.is_none() + && is_timelock_ready( + monitor.height, + num_blocks_per_network(get_network(), OPERATOR_ACK_TIMELOCK) as i64, + current_height, + ) + { + any_nack_ready = true; + } + } + Some(_) => {} + None => { + all_watchtower_connectors_spent = false; + if ack_spent_txid.is_none() + && is_timelock_ready( + monitor.height, + num_blocks_per_network(get_network(), WATCHTOWER_CHALLENGE_TIMELOCK) as i64, + current_height, + ) + { + any_watchtower_timeout_ready = true; + } } - return Ok(None); } - }; - if spent_txid == take1_txid { - info!( - "process_kickoff_graph graph_id:{}, take1 is on chain, will try call contract", - graph.graph_id - ); - Ok(Some(( - Actor::Committee, - GOATMessageContent::Take1Sent(Take1Sent { + } + + if any_watchtower_timeout_ready { + messages.push(DetectedGraphMessage::new( + Actor::Operator, + GOATMessageContent::WatchtowerChallengeTimeout(WatchtowerChallengeTimeout { instance_id: graph.instance_id, graph_id: graph.graph_id, }), - ))) - } else { - info!( - "process_kickoff_graph graph_id:{}, challenge txid: {} has been detected.", - graph.graph_id, - spent_txid.to_string() - ); - // Challenge was sent - Ok(Some(( - Actor::Operator, - GOATMessageContent::ChallengeSent(ChallengeSent { + )); + } + + if any_nack_ready { + messages.push(DetectedGraphMessage::new( + Actor::Verifier, + GOATMessageContent::NackReady(NackReady { instance_id: graph.instance_id, graph_id: graph.graph_id, - challenge_txid: spent_txid, }), - ))) + )); } -} - -/// Process watchtower challenge monitoring -/// may trigger: WatchtowerChallengeSent, WatchtowerChallengeTimeout, OperatorAckTimeout, DisproveSent(OperatorCommitTimeout/OperatorNack), OperatorCommitBlockHashReady, OperatorCommitBlockHashTimeout -/// return Ok(true) if watchtower challenge is success -#[tracing::instrument(level = "info", skip(btc_client, local_db))] -async fn process_watchtower_challenge_monitoring( - btc_client: &BTCClient, - local_db: &LocalDB, - graph: &Graph, - _sub_status: &mut ChallengeSubStatus, - current_height: i64, -) -> anyhow::Result { - // WatchtowerChallengeInitSent will be pushed inside refresh_watchtower_challenge_monitor_data - let (mut vout_monitor_data, watchtower_challenge_init_height, monitor_result) = - match refresh_watchtower_challenge_monitor_data(local_db, btc_client, graph).await? { - Some((data, init_height, monitor_result)) => (data, init_height, monitor_result), - None => { - warn!( - "graph_id {} fail to get vout monitor data, maybe watchtower-challenge-init-tx not confirmed yet", - graph.graph_id - ); - return Ok(false); - } - }; - let timelock_config = get_challenge_timelock_config(); - info!("timelock_config: {timelock_config:?}"); - let is_challenge_timeout = watchtower_challenge_init_height - + timelock_config.watchtower_challenge_timelock - < current_height; - let is_ack_timeout = - watchtower_challenge_init_height + timelock_config.watchtower_ack_timelock < current_height; - let is_blockhash_commit_timeout = watchtower_challenge_init_height - + timelock_config.watchtower_blockhash_commit_timelock - < current_height; - info!( - "is_ack_timeout_{is_ack_timeout}, is_challenge_timeout_{is_challenge_timeout}, \ - is_blockhash_commit_timeout_{is_blockhash_commit_timeout}, watchtower_challenge_init_height:{watchtower_challenge_init_height}, current_height:{current_height} ", - ); - if vout_monitor_data.is_watchtower_challenge_success() { - info!("graph_id {} watchtower challenge is success", graph.graph_id); - return Ok(true); - } - if vout_monitor_data.is_disproved() { - let challenge_start_txid = graph.challenge_txid.clone().map(|v| v.into()); - let (disprove_type, index, challenge_finish_txid) = if vout_monitor_data - .commit_blockhash_status - == CommitBlockHashStatus::OperatorCommitTimeout - { - let blockhash_commit_timeout_txid = graph.blockhash_commit_timeout_txid.clone().ok_or_else(|| anyhow::anyhow!( - "graph_id {} is disproved by OperatorCommitTimeout but blockhash_commit_timeout_txid is none", - graph.graph_id - ))?; - (DisproveTxType::OperatorCommitTimeout, 0, blockhash_commit_timeout_txid.into()) - } else if let Some((&index, _)) = vout_monitor_data - .data_map - .iter() - .find(|(_, status)| **status == WatchtowerChallengeItemStatus::OperatorNACK) - { - let nack_txid = graph.nack_txids.get(index as usize).cloned().ok_or_else(|| { - anyhow::anyhow!( - "graph_id {} is disproved by OperatorNACK at index {} but nack_txid is none", - graph.graph_id, - index - ) - })?; - (DisproveTxType::OperatorNack, index as usize, nack_txid.into()) - } else { - anyhow::bail!( - "graph_id {} vout monitor data is disproved but no OperatorNACK or OperatorCommitBlockHashTimeout found", - graph.graph_id - ) - }; - upsert_message( - &mut local_db.acquire().await?, - false, - graph.graph_id, - None, - SELF_SENDER.to_string(), + if all_watchtower_connectors_spent + && connector_e_spent_txid.is_none() + && connector_f_spent_txid.is_none() + { + messages.push(DetectedGraphMessage::new( Actor::Operator, - GOATMessageContent::DisproveSent(DisproveSent { + GOATMessageContent::OperatorCommitPubinReady(OperatorCommitPubinReady { instance_id: graph.instance_id, graph_id: graph.graph_id, - disprove_type, - index, - challenge_start_txid, - challenge_finish_txid, }), - 0, - 0, - ) - .await?; - return Ok(false); // already disproved, no need to process further + )); + + if is_timelock_ready( + monitor.height, + num_blocks_per_network(get_network(), OPERATOR_COMMIT_TIMELOCK) as i64, + current_height, + ) { + messages.push(DetectedGraphMessage::new( + Actor::Verifier, + GOATMessageContent::OperatorCommitPubinTimeout(OperatorCommitPubinTimeout { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + }), + )); + } } - if !vout_monitor_data.is_commit_blockhash_processed() - && vout_monitor_data.is_commit_blockhash_ready() + + if all_watchtower_connectors_spent + && pubin_commit_completed + && let Some(operator_assert_txid) = graph.operator_assert_txid.clone() + && !btc_client.get_tx_status(&operator_assert_txid.0).await?.confirmed { - upsert_message( - &mut local_db.acquire().await?, - false, - graph.graph_id, - None, - SELF_SENDER.to_string(), + messages.push(DetectedGraphMessage::new( Actor::Operator, - GOATMessageContent::OperatorCommitBlockHashReady(OperatorCommitBlockHashReady { + GOATMessageContent::AssertReady(AssertReady { instance_id: graph.instance_id, graph_id: graph.graph_id, }), - 0, - 0, - ) - .await?; + )); } - if !vout_monitor_data.is_commit_blockhash_processed() && is_blockhash_commit_timeout { - upsert_message( - &mut local_db.acquire().await?, - false, + + Ok(messages) +} + +/// may trigger: +/// - AssertSent +/// - ChallengeAssertSent +async fn detect_assert_sent_flow( + btc_client: &BTCClient, + local_db: &LocalDB, + graph: &Graph, +) -> anyhow::Result> { + let operator_assert_txid: Txid = match graph.operator_assert_txid.clone() { + Some(txid) => txid.into(), + None => { + warn!( + "detect_assert_sent_flow graph_id:{} operator_assert_txid is none", + graph.graph_id + ); + return Ok(vec![]); + } + }; + + let Some(_) = get_confirmed_tx_monitor( + btc_client, + local_db, + graph.graph_id, + operator_assert_txid, + MONITE_BTC_TX_NAME_PROVER_ASSERT.to_string(), + ) + .await? + else { + trace!( + "detect_assert_sent_flow graph_id:{} operator assert txid {} not confirmed", + graph.graph_id, operator_assert_txid + ); + return Ok(vec![]); + }; + + let mut messages = vec![DetectedGraphMessage::new( + Actor::Verifier, + GOATMessageContent::AssertSent(AssertSent { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + assert_txid: operator_assert_txid, + assert_witness: None, + }), + )]; + + for (verifier_index, challenge_assert_txid) in graph.verifier_assert_txids.iter().enumerate() { + let challenge_assert_txid: Txid = challenge_assert_txid.clone().into(); + let Some(_) = get_confirmed_tx_monitor( + btc_client, + local_db, graph.graph_id, - None, - SELF_SENDER.to_string(), - Actor::Challenger, - GOATMessageContent::OperatorCommitBlockHashTimeout(OperatorCommitBlockHashTimeout { + challenge_assert_txid, + format!("{MONITE_BTC_TX_NAME_VERIFIER_ASSERT}_{verifier_index}"), + ) + .await? + else { + continue; + }; + + messages.push(DetectedGraphMessage::with_sub_type( + Actor::Operator, + GOATMessageContent::ChallengeAssertSent(ChallengeAssertSent { instance_id: graph.instance_id, graph_id: graph.graph_id, + challenge_assert_txid, + verifier_index, + challenge_witness: None, }), - 0, - 0, - ) - .await?; + verifier_index.to_string(), + )); } - if vout_monitor_data - .data_map - .values() - .any(|status| *status == WatchtowerChallengeItemStatus::Challenge) - { - let watchtower_challenge_txids = monitor_result.0; - if watchtower_challenge_txids.is_empty() { + + Ok(messages) +} + +// may trigger disprove ready +async fn detect_assert_disprove_ready( + btc_client: &BTCClient, + local_db: &LocalDB, + graph: &Graph, + current_height: i64, +) -> anyhow::Result)>> { + let operator_assert_txid = match graph.operator_assert_txid.clone() { + Some(operator_assert_txid) => operator_assert_txid.into(), + None => { warn!( - "graph_id {} watchtower challenge txids is empty when some vout status is Challenge", + "detect_assert_disprove_ready graph_id:{} operator_assert_txid has none value", graph.graph_id ); - } else { - upsert_message( - &mut local_db.acquire().await?, - false, - graph.graph_id, - None, - SELF_SENDER.to_string(), - Actor::Operator, - GOATMessageContent::WatchtowerChallengeSent(WatchtowerChallengeSent { - instance_id: graph.instance_id, - graph_id: graph.graph_id, - watchtower_challenge_txids, - }), - 0, - 0, - ) - .await?; + return Ok(None); } + }; + if graph.verifier_assert_txids.is_empty() { + return Ok(None); } - if is_ack_timeout { - // Re-sends an OperatorAckTimeout message whenever the disproved indexes change. - vout_monitor_data.update_disprove_indexes(); - if !vout_monitor_data.require_disproved_indexes.is_empty() { - upsert_message( - &mut local_db.acquire().await?, - false, - graph.graph_id, - Some(vout_monitor_data.get_require_disproved_string()), - SELF_SENDER.to_string(), - Actor::Challenger, - GOATMessageContent::OperatorAckTimeout(OperatorAckTimeout { - instance_id: graph.instance_id, - graph_id: graph.graph_id, - }), - 0, - 0, - ) - .await?; - } + + let connector_d_vout = + output_topology::operator_assert::connector_d(graph.verifier_assert_txids.len()) as u64; + if outpoint_spent_txid(btc_client, &operator_assert_txid, connector_d_vout).await?.is_some() { + trace!( + "detect_assert_disprove_ready graph_id:{} connector_d already spent", + graph.graph_id + ); + return Ok(None); } - if is_challenge_timeout { - // Re-sends an WatchtowerChallengeTimeout message whenever the watchtower indexes change. - let watchtower_indexes: Vec = vout_monitor_data - .data_map - .iter() - .filter_map(|(&index, status)| match status { - WatchtowerChallengeItemStatus::OperatorInit => Some(index as usize), - _ => None, - }) - .collect(); - if !watchtower_indexes.is_empty() { - let sub_type = format!( - "[{}]", - watchtower_indexes.iter().map(|v| v.to_string()).collect::>().join("_") + + for (index, verifier_assert_txid) in graph.verifier_assert_txids.iter().enumerate() { + let verifier_assert_txid: Txid = verifier_assert_txid.clone().into(); + if outpoint_spent_txid(btc_client, &verifier_assert_txid, 0).await?.is_some() { + continue; + } + + let height = { + let mut storage_processor = local_db.acquire().await?; + storage_processor + .find_graph_btc_tx_vout_monitor(&graph.graph_id, &verifier_assert_txid.into()) + .await? + .unwrap_or_default() + .height + }; + let height = if height <= 0 { + let Some(tx_info) = btc_client.get_tx_info(&verifier_assert_txid).await? else { + continue; + }; + let height = tx_info.status.block_height.unwrap_or_default() as i64; + if height <= 0 { + continue; + } + + let current_times = current_time_secs(); + let mut storage_processor = local_db.acquire().await?; + storage_processor + .upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { + graph_id: graph.graph_id, + tx_name: format!("verifier_assert_{index}"), + txid: verifier_assert_txid.into(), + height, + vout_len: tx_info.vout.len() as i64, + monitor_data: String::new(), + created_at: current_times, + updated_at: current_times, + }) + .await?; + height + } else { + height + }; + + if height + get_disprove_timelock_config() <= current_height { + info!( + "detect_assert_disprove_ready graph_id:{} verifier_assert index:{} is ready to disprove", + graph.graph_id, index ); - upsert_message( - &mut local_db.acquire().await?, - false, - graph.graph_id, - Some(sub_type), - SELF_SENDER.to_string(), - Actor::Operator, - GOATMessageContent::WatchtowerChallengeTimeout(WatchtowerChallengeTimeout { + return Ok(Some(( + Actor::Verifier, + GOATMessageContent::WronglyChallengeTimeout(WronglyChallengeTimeout { instance_id: graph.instance_id, graph_id: graph.graph_id, - watchtower_indexes, + challenge_assert_txid: verifier_assert_txid, + verifier_index: index, + wrongly_challenged_witness: None, }), - 0, - 0, - ) - .await?; + Some(index.to_string()), + ))); } } - Ok(false) + + Ok(None) } -/// Process assert commit monitoring -/// may trigger: DisproveSent(AssertTimeout), AssertCommitTimeout -/// return Ok(true) if assert commit is success -async fn process_assert_commit_monitoring( +/// Check if Take1Ready Take2Ready message needs to be sent +async fn check_operator_withdraw_ready_condition( btc_client: &BTCClient, local_db: &LocalDB, - graph: &Graph, - _sub_status: &mut ChallengeSubStatus, + graph_id: Uuid, + check_tx_items: Vec<(Txid, String, OperatorWithdrawType, i64, i64)>, // (txid, tag, height, lock_blocks) current_height: i64, ) -> anyhow::Result { - let (mut vout_monitor_data, assert_init_height, _monitor_result) = - match refresh_assert_monitor_data(local_db, btc_client, graph).await? { - Some((data, init_height, monitor_result)) => (data, init_height, monitor_result), - None => { - warn!( - "graph_id {} fail to get vout monitor data, maybe assert-init-tx not confirmed yet", + info!( + "check_operator_withdraw_ready_condition for graph_id: {graph_id}, check tx size: {}, detail:{check_tx_items:?}", + check_tx_items.len() + ); + let mut ready = true; + for (txid, tx_name, operator_withdraw_type, height, lock_blocks) in check_tx_items { + let height = if height <= 0 { + let current_times = current_time_secs(); + let (height, vout_len) = match btc_client.get_tx_info(&txid).await? { + Some(tx_info) => ( + tx_info.status.block_height.unwrap_or_default() as i64, + tx_info.vout.len() as i64, + ), + None => { + info!("graph_id:{graph_id}, {operator_withdraw_type} txid {txid} not on chain",); + return Ok(false); + } + }; + let txid_serial: SerializableTxid = txid.into(); + let mut storage_processor = local_db.acquire().await?; + let existing = + storage_processor.find_graph_btc_tx_vout_monitor(&graph_id, &txid_serial).await?; + let (monitor_data, created_at, tx_name_to_use) = match existing { + Some(existing) => (existing.monitor_data, existing.created_at, existing.tx_name), + None => ("".to_string(), current_times, tx_name.clone()), + }; + storage_processor + .upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { + graph_id, + tx_name: tx_name_to_use, + txid: txid_serial, + height, + vout_len, + monitor_data, + created_at, + updated_at: current_times, + }) + .await?; + height + } else { + height + }; + + info!( + "graph_id:{graph_id}, {operator_withdraw_type} txid {txid} at height {height} lock_blocks {lock_blocks}, current height: {current_height} ", + ); + + if height == 0 || height > 0 && height + lock_blocks > current_height { + ready = false; + break; + } + } + Ok(ready) +} + +/// Process graph data in KickOff status +/// may return: Take1Ready, Take1Sent, ChallengeSent +async fn process_kickoff_graph( + btc_client: &BTCClient, + local_db: &LocalDB, + graph: &Graph, + lock_blocks: i64, + current_height: i64, +) -> anyhow::Result> { + trace!("process_kickoff_graph: {}", graph.graph_id); + let (kickoff_txid, take1_txid) = match (graph.kickoff_txid.clone(), graph.take1_txid.clone()) { + (Some(kickoff), Some(take1)) => (kickoff.into(), take1.into()), + _ => { + // NOTE: revert to previous status? + warn!("process_kickoff_graph graph_id:{}, kickoff or take1 is none", graph.graph_id); + return Ok(None); + } + }; + let connector_a_vout = output_topology::kickoff::connector_a() as u64; + let spent_txid = match outpoint_spent_txid(btc_client, &kickoff_txid, connector_a_vout).await? { + Some(txid) => txid, + None => { + // kickoff output not spent, check if we need to send Take1Ready + let height = { + let mut storage_processor = local_db.acquire().await?; + storage_processor + .find_graph_btc_tx_vout_monitor(&graph.graph_id, &kickoff_txid.into()) + .await? + .unwrap_or_default() + .height + }; + if check_operator_withdraw_ready_condition( + btc_client, + local_db, + graph.graph_id, + vec![( + kickoff_txid, + MONITE_BTC_TX_NAME_KICKOFF.to_string(), + OperatorWithdrawType::Take1, + height, + lock_blocks, + )], + current_height, + ) + .await? + { + info!( + "process_kickoff_graph graph_id:{}, take1 is ready to send to btc chain", graph.graph_id ); - return Ok(false); + return Ok(Some(( + Actor::Operator, + GOATMessageContent::Take1Ready(Take1Ready { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + }), + ))); + } else { + trace!("process_kickoff_graph graph_id:{}, take1 not ready", graph.graph_id); } - }; - - let timelock_config = get_challenge_timelock_config(); - let is_assert_commit_timeout = - assert_init_height + timelock_config.assert_commit_timelock < current_height; - info!( - "process_assert_commit_monitoring: graph id :{} is_assert_commit_timeout:{is_assert_commit_timeout},\ - assert_init_height:{assert_init_height}, timelock_config.assert_commit_timelock:{}, current_height:{current_height}", - graph.graph_id, timelock_config.assert_commit_timelock - ); - if vout_monitor_data.is_assert_success() { - info!("graph_id {} assert commit is success", graph.graph_id); - return Ok(true); - } - if vout_monitor_data.is_disproved() { - let challenge_start_txid = graph.challenge_txid.clone().map(|v| v.into()); - let disprove_type = DisproveTxType::AssertTimeout; - let index = vout_monitor_data - .data_map - .iter() - .find(|(_, status)| **status == AssertCommitItemStatus::OperatorCommitTimeout) - .map(|(&index, _)| index as usize) - .ok_or_else(|| anyhow::anyhow!("graph_id {} assert vout monitor data is disproved but no OperatorCommitTimeout found", graph.graph_id))?; - let challenge_finish_txid = graph.assert_commit_timeout_txids.get(index).cloned().ok_or_else(|| anyhow::anyhow!( - "graph_id {} is disproved by OperatorCommitTimeout at index {} but assert_commit_timeout_txid is none", - graph.graph_id, - index - ))?.into(); - upsert_message( - &mut local_db.acquire().await?, - false, + return Ok(None); + } + }; + if spent_txid == take1_txid { + info!( + "process_kickoff_graph graph_id:{}, take1 is on chain, will try call contract", + graph.graph_id + ); + Ok(Some(( + Actor::Committee, + GOATMessageContent::Take1Sent(Take1Sent { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + }), + ))) + } else { + info!( + "process_kickoff_graph graph_id:{}, challenge txid: {} has been detected.", graph.graph_id, - None, - SELF_SENDER.to_string(), + spent_txid.to_string() + ); + // Challenge was sent + Ok(Some(( Actor::Operator, - GOATMessageContent::DisproveSent(DisproveSent { + GOATMessageContent::ChallengeSent(ChallengeSent { instance_id: graph.instance_id, graph_id: graph.graph_id, - disprove_type, - index, - challenge_start_txid, - challenge_finish_txid, + challenge_txid: spent_txid, }), - 0, - 0, - ) - .await?; - return Ok(false); // already disproved, no need to process further - } - if is_assert_commit_timeout { - vout_monitor_data.update_disprove_indexes(); - if !vout_monitor_data.require_disproved_indexes.is_empty() { - upsert_message( - &mut local_db.acquire().await?, - false, - graph.graph_id, - Some(vout_monitor_data.get_require_disproved_string()), - SELF_SENDER.to_string(), - Actor::Challenger, - GOATMessageContent::AssertCommitTimeout(AssertCommitTimeout { - instance_id: graph.instance_id, - graph_id: graph.graph_id, - }), - 0, - 0, - ) - .await?; - } + ))) } - - Ok(false) } /// may trigger: DisproveSent(QuickChallenge/ChallengeIncompleteKickoff) @@ -1220,23 +970,9 @@ async fn detect_kickoff_ref_disprove_tx( info!("graph_id:{} next {pre_sents} graphs's pre_kickoff has been sent!", graph.graph_id); detected = true; } - let out_monitor = { - let mut storage_processor = local_db.acquire().await?; - storage_processor - .find_graph_btc_tx_vout_monitor(&graph.graph_id, &kickoff_txid.into()) - .await? - }; - - let Some(out_monitor) = out_monitor else { - return Ok(false); - }; - - if let Some(spend_txid) = outpoint_spent_txid( - btc_client, - &kickoff_txid, - out_monitor.vout_len as u64 - CONNECTOR_GUARDIAN_MARGIN, - ) - .await? + let guardian_connector_vout = output_topology::kickoff::guardian_connector() as u64; + if let Some(spend_txid) = + outpoint_spent_txid(btc_client, &kickoff_txid, guardian_connector_vout).await? && let Some(tx) = btc_client.get_tx(&spend_txid).await? { if spend_txid == take1_txid || spend_txid == take2_txid || tx.input.len() < 2 { @@ -1279,7 +1015,6 @@ async fn detect_kickoff_ref_disprove_tx( Ok(detected) } -/// Process graph data in Watchtower Assert Normal status /// may trigger: Take2Ready, Take2Sent, DisproveSent(Disprove) async fn detect_take2( btc_client: &BTCClient, @@ -1287,126 +1022,123 @@ async fn detect_take2( graph: &Graph, current_height: i64, ) -> anyhow::Result> { - trace!("detect_take2 graph_id {}", graph.graph_id); - let timelock_config = get_take2_timelock_config(); - let (kickoff_txid, watchtower_challenge_init_txid, assert_init_txid, take2_txid): ( - Txid, - Txid, - Txid, - Txid, - ) = match ( + let (kickoff_txid, operator_assert_txid, take2_txid) = match ( graph.kickoff_txid.clone(), - graph.watchtower_challenge_init_txid.clone(), - graph.assert_init_txid.clone(), + graph.operator_assert_txid.clone(), graph.take2_txid.clone(), ) { - ( - Some(kickoff_txid), - Some(watchtower_challenge_init_txid), - Some(assert_init_txid), - Some(take2_txid), - ) => ( - kickoff_txid.into(), - watchtower_challenge_init_txid.into(), - assert_init_txid.into(), - take2_txid.into(), - ), + (Some(kickoff_txid), Some(operator_assert_txid), Some(take2_txid)) => { + (kickoff_txid.into(), operator_assert_txid.into(), take2_txid.into()) + } _ => { warn!( - "detect_take2 graph_id:{}, kickoff_txid, watchtower_challenge_init_txid, assert_init_txid or take2_txid is none", + "detect_take2 graph_id:{} kickoff_txid/operator_assert_txid/take2_txid has none value", graph.graph_id ); return Ok(None); } }; - let spent_txid = match outpoint_spent_txid(btc_client, &kickoff_txid, 3).await? { - Some(txid) => txid, - None => { - trace!("detect_take2 graph_id {} take2 or disprove is not on chain", graph.graph_id); - let mut storage_processor = local_db.acquire().await?; - let watchtower_init_height = storage_processor - .find_graph_btc_tx_vout_monitor( - &graph.graph_id, - &watchtower_challenge_init_txid.into(), - ) - .await? - .unwrap_or_default() - .height; - - let assert_init_height = storage_processor - .find_graph_btc_tx_vout_monitor(&graph.graph_id, &assert_init_txid.into()) - .await? - .unwrap_or_default() - .height; - let ready = check_operator_withdraw_ready_condition( - btc_client, - local_db, - graph.graph_id, - vec![ - ( - watchtower_challenge_init_txid, - MONITE_BTC_TX_NAME_WATCHTOWER_INIT.to_string(), - OperatorWithdrawType::Take2, - watchtower_init_height, - timelock_config.watchtower_challenge_init_out_timelock, - ), - ( - assert_init_txid, - MONITE_BTC_TX_NAME_ASSERT_INIT.to_string(), - OperatorWithdrawType::Take2, - assert_init_height, - timelock_config.assert_init_out_timelock, - ), - ], - current_height, - ) - .await?; + let connector_d_vout = + output_topology::operator_assert::connector_d(graph.verifier_assert_txids.len()) as u64; + if let Some(spend_txid) = + outpoint_spent_txid(btc_client, &operator_assert_txid, connector_d_vout).await? + { + if spend_txid == take2_txid { + info!("detect_take2 graph_id:{} take2 is on chain", graph.graph_id); + return Ok(Some(( + Actor::Committee, + GOATMessageContent::Take2Sent(Take2Sent { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + }), + ))); + } - if ready { + if let Some(tx) = btc_client.get_tx(&spend_txid).await? + && tx.input.len() == 2 + { + let verifier_assert_txid = tx.input[0].previous_output.txid; + if let Some(index) = graph + .verifier_assert_txids + .iter() + .position(|txid| Txid::from(txid.clone()) == verifier_assert_txid) + { info!( - "detect_take2 graph_id {} take2 is ready to send to btc chain", - graph.graph_id + "detect_take2 graph_id:{} disprove is on chain, spent txid:{}, index:{}", + graph.graph_id, spend_txid, index ); return Ok(Some(( - Actor::Operator, - GOATMessageContent::Take2Ready(Take2Ready { + Actor::Committee, + GOATMessageContent::DisproveSent(DisproveSent { instance_id: graph.instance_id, graph_id: graph.graph_id, + disprove_type: DisproveTxType::Disprove, + index, + challenge_start_txid: graph.challenge_txid.clone().map(|v| v.into()), + challenge_finish_txid: spend_txid, }), ))); } - return Ok(None); } - }; - if spent_txid == take2_txid { - info!( - "detect_take2 graph_id {} take2:{} is on btc chain", - graph.graph_id, - spent_txid.to_string() + warn!( + "detect_take2 graph_id:{} connector_d spent by pubin-disprove txid:{}", + graph.graph_id, spend_txid ); - Ok(Some(( - Actor::All, - GOATMessageContent::Take2Sent(Take2Sent { + return Ok(Some(( + Actor::Committee, + GOATMessageContent::DisproveSent(DisproveSent { instance_id: graph.instance_id, graph_id: graph.graph_id, + disprove_type: DisproveTxType::PubinDisprove, + index: 0, + challenge_start_txid: None, + challenge_finish_txid: spend_txid, }), - ))) - } else { - let disprove_type = DisproveTxType::Disprove; - let challenge_start_txid = graph.challenge_txid.clone().map(|v| v.into()); + ))); + } + + let guardian_connector_vout = output_topology::kickoff::guardian_connector() as u64; + if outpoint_spent_txid(btc_client, &kickoff_txid, guardian_connector_vout).await?.is_some() { + trace!("detect_take2 graph_id:{} guardian connector already spent", graph.graph_id); + return Ok(None); + } + + let height = { + let mut storage_processor = local_db.acquire().await?; + storage_processor + .find_graph_btc_tx_vout_monitor(&graph.graph_id, &operator_assert_txid.into()) + .await? + .unwrap_or_default() + .height + }; + if check_operator_withdraw_ready_condition( + btc_client, + local_db, + graph.graph_id, + vec![( + operator_assert_txid, + MONITE_BTC_TX_NAME_PROVER_ASSERT.to_string(), + OperatorWithdrawType::Take2, + height, + get_take2_timelock_config(), + )], + current_height, + ) + .await? + { + info!("detect_take2 graph_id:{} take2 is ready to send to btc chain", graph.graph_id); Ok(Some(( - Actor::All, - GOATMessageContent::DisproveSent(DisproveSent { + Actor::Operator, + GOATMessageContent::Take2Ready(Take2Ready { instance_id: graph.instance_id, graph_id: graph.graph_id, - disprove_type, - index: 0, - challenge_start_txid, - challenge_finish_txid: spent_txid, }), ))) + } else { + trace!("detect_take2 graph_id:{} take2 not ready", graph.graph_id); + Ok(None) } } @@ -1448,7 +1180,7 @@ async fn check_pre_kickoff_sent( graph_id, None, SELF_SENDER.to_string(), - Actor::Challenger, + Actor::Verifier, GOATMessageContent::PreKickoffSent(PreKickoffSent { instance_id, graph_id }), 0, 0, @@ -1459,302 +1191,3 @@ async fn check_pre_kickoff_sent( } Ok(pre_sents) } - -pub(crate) async fn refresh_watchtower_challenge_monitor_data( - local_db: &LocalDB, - btc_client: &BTCClient, - graph: &Graph, -) -> anyhow::Result< - Option<( - WTInitTxVoutMonitorData, - i64, - (Vec<(usize, Txid)>, Vec<(usize, Txid)>, Vec<(usize, Txid)>), - )>, -> { - let watchtower_challenge_init_txid = - graph.watchtower_challenge_init_txid.clone().ok_or_else(|| { - anyhow::anyhow!("graph_id:{} watchtower_challenge_init_txid is none", graph.graph_id) - })?; - let out_monitor = { - let mut storage_processor = local_db.acquire().await?; - storage_processor - .find_graph_btc_tx_vout_monitor(&graph.graph_id, &watchtower_challenge_init_txid) - .await? - }; - - let mut init_height; - let mut monitor_meta_update: Option<(i64, i64)> = None; - let mut existing_meta: Option<(String, i64)> = None; - let mut vout_monitor_data = if let Some(out_monitor) = out_monitor { - existing_meta = Some((out_monitor.tx_name.clone(), out_monitor.created_at)); - init_height = out_monitor.height; - match parse_monitor_data::(&out_monitor.monitor_data) { - Ok(vout_monitor_data) => vout_monitor_data, - Err(err) => { - warn!( - "graph_id:{} fail to parse watchtower monitor_data, rebuild default: {err}", - graph.graph_id - ); - let mut new_height = out_monitor.height; - let mut new_vout_len = out_monitor.vout_len; - if new_height <= 0 || new_vout_len <= 0 { - let txid: Txid = watchtower_challenge_init_txid.clone().into(); - if let Some(tx) = btc_client.get_tx_info(&txid).await? { - if new_height <= 0 { - new_height = tx.status.block_height.unwrap_or_default() as i64; - } - if new_vout_len <= 0 { - new_vout_len = tx.vout.len() as i64; - } - } - } - init_height = new_height; - let index_size = (new_vout_len as i32 - CONNECTOR_G_MARGIN as i32) / 2; - if index_size <= 0 { - warn!( - "graph_id:{} watchtower_challenge_init_txid {} invalid index_size {index_size}, skip refresh", - graph.graph_id, watchtower_challenge_init_txid.0 - ); - return Ok(None); - } - if new_height != out_monitor.height || new_vout_len != out_monitor.vout_len { - monitor_meta_update = Some((new_height, new_vout_len)); - } - WTInitTxVoutMonitorData::new(index_size) - } - } - } else { - let txid: Txid = watchtower_challenge_init_txid.clone().into(); - let watchtower_challenge_init_tx = match btc_client - .get_tx_info(&txid) - .await? - .filter(|tx| tx.status.block_height.unwrap_or_default() > 0) - { - Some(tx) => tx, - None => { - warn!( - "graph_id:{} watchtower_challenge_init_txid not on chain, skip refresh", - graph.graph_id - ); - return Ok(None); - } - }; - - init_height = watchtower_challenge_init_tx.status.block_height.unwrap_or_default() as i64; - let vout_monitor_data = WTInitTxVoutMonitorData::new( - (watchtower_challenge_init_tx.vout.len() as i32 - CONNECTOR_G_MARGIN as i32) / 2, - ); - - let current_times = current_time_secs(); - let mut tx = local_db.start_transaction().await?; - tx.upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { - graph_id: graph.graph_id, - tx_name: MONITE_BTC_TX_NAME_WATCHTOWER_INIT.to_string(), - txid: watchtower_challenge_init_txid.clone(), - height: watchtower_challenge_init_tx.status.block_height.unwrap_or_default() as i64, - vout_len: watchtower_challenge_init_tx.vout.len() as i64, - monitor_data: serde_json::to_string(&vout_monitor_data)?, - created_at: current_times, - updated_at: current_times, - }) - .await?; - tx.commit().await?; - - vout_monitor_data - }; - - if init_height <= 0 { - warn!( - "graph_id:{} watchtower_challenge_init_txid {} height is not confirmed, skip refresh", - graph.graph_id, watchtower_challenge_init_txid.0 - ); - return Ok(None); - } - let block_hash_commit_timeout_txid = - graph.blockhash_commit_timeout_txid.clone().ok_or_else(|| { - anyhow::anyhow!("graph_id:{} blockhash_commit_timeout_txid is empty", graph.graph_id) - })?; - let monitor_result = vout_monitor_data - .monitor_vout( - btc_client, - &watchtower_challenge_init_txid.clone().into(), - &graph.watchtower_challenge_timeout_txids, - &graph.nack_txids, - &block_hash_commit_timeout_txid, - ) - .await?; - let mut tx = local_db.start_transaction().await?; - if let (Some((height, vout_len)), Some((tx_name, created_at))) = - (monitor_meta_update, existing_meta) - { - tx.upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { - graph_id: graph.graph_id, - tx_name, - txid: watchtower_challenge_init_txid.clone(), - height, - vout_len, - monitor_data: serde_json::to_string(&vout_monitor_data)?, - created_at, - updated_at: current_time_secs(), - }) - .await?; - } else { - tx.update_graph_btc_tx_vout_monitor_data( - &graph.graph_id, - &watchtower_challenge_init_txid, - serde_json::to_string(&vout_monitor_data)?, - ) - .await?; - } - // Always insert WatchtowerChallengeInitSent to avoid missing; repeated inserts are idempotent - upsert_message( - &mut tx, - false, - graph.graph_id, - None, - SELF_SENDER.to_string(), - Actor::Watchtower, - GOATMessageContent::WatchtowerChallengeInitSent(WatchtowerChallengeInitSent { - instance_id: graph.instance_id, - graph_id: graph.graph_id, - }), - 0, - 0, - ) - .await?; - tx.commit().await?; - - Ok(Some((vout_monitor_data, init_height, monitor_result))) -} - -pub(crate) async fn refresh_assert_monitor_data( - local_db: &LocalDB, - btc_client: &BTCClient, - graph: &Graph, -) -> anyhow::Result> { - let assert_init_txid = graph - .assert_init_txid - .clone() - .ok_or_else(|| anyhow::anyhow!("graph_id:{} assert_init_txid is none", graph.graph_id))?; - let out_monitor = { - let mut storage_processor = local_db.acquire().await?; - storage_processor.find_graph_btc_tx_vout_monitor(&graph.graph_id, &assert_init_txid).await? - }; - - let mut init_height; - let mut monitor_meta_update: Option<(i64, i64)> = None; - let mut existing_meta: Option<(String, i64)> = None; - let mut vout_monitor_data = if let Some(out_monitor) = out_monitor { - existing_meta = Some((out_monitor.tx_name.clone(), out_monitor.created_at)); - init_height = out_monitor.height; - match parse_monitor_data::(&out_monitor.monitor_data) { - Ok(vout_monitor_data) => vout_monitor_data, - Err(err) => { - warn!( - "graph_id:{} fail to parse assert monitor_data, rebuild default: {err}", - graph.graph_id - ); - let mut new_height = out_monitor.height; - let mut new_vout_len = out_monitor.vout_len; - if new_height <= 0 || new_vout_len <= 0 { - let txid: Txid = assert_init_txid.clone().into(); - if let Some(tx) = btc_client.get_tx_info(&txid).await? { - if new_height <= 0 { - new_height = tx.status.block_height.unwrap_or_default() as i64; - } - if new_vout_len <= 0 { - new_vout_len = tx.vout.len() as i64; - } - } - } - init_height = new_height; - let index_size = new_vout_len as i32 - 2; - if index_size <= 0 { - warn!( - "graph_id:{} assert_init_txid {} invalid index_size {index_size}, skip refresh", - graph.graph_id, assert_init_txid.0 - ); - return Ok(None); - } - if new_height != out_monitor.height || new_vout_len != out_monitor.vout_len { - monitor_meta_update = Some((new_height, new_vout_len)); - } - AssertInitTxVoutMonitorData::new(index_size) - } - } - } else { - let txid: Txid = assert_init_txid.clone().into(); - let assert_init_tx = match btc_client - .get_tx_info(&txid) - .await? - .filter(|tx| tx.status.block_height.unwrap_or_default() > 0) - { - Some(tx) => tx, - None => { - warn!("graph_id:{} assert_init_txid not on chain, skip refresh", graph.graph_id); - return Ok(None); - } - }; - - init_height = assert_init_tx.status.block_height.unwrap_or_default() as i64; - let vout_monitor_data = - AssertInitTxVoutMonitorData::new(assert_init_tx.vout.len() as i32 - 2); - let current_times = current_time_secs(); - let mut tx = local_db.start_transaction().await?; - tx.upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { - graph_id: graph.graph_id, - tx_name: MONITE_BTC_TX_NAME_ASSERT_INIT.to_string(), - txid: assert_init_txid.clone(), - height: assert_init_tx.status.block_height.unwrap_or_default() as i64, - vout_len: assert_init_tx.vout.len() as i64, - monitor_data: serde_json::to_string(&vout_monitor_data)?, - created_at: current_times, - updated_at: current_times, - }) - .await?; - tx.commit().await?; - - vout_monitor_data - }; - - if init_height <= 0 { - warn!( - "graph_id:{} assert_init_txid {} height is not confirmed, skip refresh", - graph.graph_id, assert_init_txid.0 - ); - return Ok(None); - } - let monitor_result = vout_monitor_data - .monitor_vout( - btc_client, - &assert_init_txid.clone().into(), - &graph.assert_commit_timeout_txids, - ) - .await?; - let mut tx = local_db.start_transaction().await?; - if let (Some((height, vout_len)), Some((tx_name, created_at))) = - (monitor_meta_update, existing_meta) - { - tx.upsert_graph_btc_tx_vout_monitor(&GraphBtcTxVoutMonitor { - graph_id: graph.graph_id, - tx_name, - txid: assert_init_txid.clone(), - height, - vout_len, - monitor_data: serde_json::to_string(&vout_monitor_data)?, - created_at, - updated_at: current_time_secs(), - }) - .await?; - } else { - tx.update_graph_btc_tx_vout_monitor_data( - &graph.graph_id, - &assert_init_txid, - serde_json::to_string(&vout_monitor_data)?, - ) - .await?; - } - tx.commit().await?; - - Ok(Some((vout_monitor_data, init_height, monitor_result))) -} diff --git a/node/src/scheduled_tasks/instance_maintenance_tasks.rs b/node/src/scheduled_tasks/instance_maintenance_tasks.rs index f2bd4071..c7dd23b7 100644 --- a/node/src/scheduled_tasks/instance_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/instance_maintenance_tasks.rs @@ -13,10 +13,10 @@ use crate::utils::{ upsert_message, }; use alloy::sol_types::SolType; -use bitvm2_lib::actors::Actor; -use bitvm2_lib::constants::CONNECTOR_Z_TIMELOCK; -use bitvm2_lib::keys::CommitteeMasterKey; -use bitvm2_lib::transactions::base::BaseTransaction; +use bitvm_lib::actors::Actor; +use bitvm_lib::constants::CONNECTOR_Z_TIMELOCK; +use bitvm_lib::keys::CommitteeMasterKey; +use bitvm_lib::transactions::base::BaseTransaction; use client::Utxo; use client::btc_chain::BTCClient; use client::goat_chain::GOATClient; diff --git a/node/src/scheduled_tasks/mod.rs b/node/src/scheduled_tasks/mod.rs index 710bd88c..7544cb99 100644 --- a/node/src/scheduled_tasks/mod.rs +++ b/node/src/scheduled_tasks/mod.rs @@ -17,7 +17,7 @@ use crate::scheduled_tasks::instance_maintenance_tasks::{ }; use crate::scheduled_tasks::node_maintenance_tasks::node_available_pbtc_update_monitor; use crate::scheduled_tasks::spv_maintenance_tasks::spv_header_hash_update; -use bitvm2_lib::actors::Actor; +use bitvm_lib::actors::Actor; use client::btc_chain::BTCClient; use client::goat_chain::GOATClient; pub use event_watch_task::{is_processing_gateway_history_events, run_watch_event_task}; @@ -159,6 +159,10 @@ pub fn get_goat_message_content_type(content: &GOATMessageContent) -> MessageTyp GOATMessageContent::PeginRequest(_) => MessageType::PeginRequest, GOATMessageContent::CreateGraph(_) => MessageType::CreateGraph, GOATMessageContent::ConfirmInstance(_) => MessageType::ConfirmInstance, + GOATMessageContent::InitGraph(_) => MessageType::InitGraph, + GOATMessageContent::GenCircuits(_) => MessageType::GenCircuits, + GOATMessageContent::CutCircuits(_) => MessageType::CutCircuits, + GOATMessageContent::SolderingProofReady(_) => MessageType::SolderingProof, GOATMessageContent::NonceGeneration(_) => MessageType::NonceGeneration, GOATMessageContent::CommitteePresign(_) => MessageType::CommitteePresign, GOATMessageContent::GraphFinalize(_) => MessageType::GraphFinalize, @@ -177,16 +181,15 @@ pub fn get_goat_message_content_type(content: &GOATMessageContent) -> MessageTyp GOATMessageContent::WatchtowerChallengeTimeout(_) => { MessageType::WatchtowerChallengeTimeout } - GOATMessageContent::OperatorAckTimeout(_) => MessageType::OperatorAckTimeout, - GOATMessageContent::OperatorCommitBlockHashReady(_) => { - MessageType::OperatorCommitBlockHashReady + GOATMessageContent::NackReady(_) => MessageType::NackReady, + GOATMessageContent::OperatorCommitPubinReady(_) => MessageType::OperatorCommitPubinReady, + GOATMessageContent::OperatorCommitPubinTimeout(_) => { + MessageType::OperatorCommitPubinTimeout } - GOATMessageContent::OperatorCommitBlockHashTimeout(_) => { - MessageType::OperatorCommitBlockHashTimeout - } - GOATMessageContent::AssertInitReady(_) => MessageType::AssertInitReady, - GOATMessageContent::AssertCommitTimeout(_) => MessageType::AssertCommitTimeout, - GOATMessageContent::DisproveReady(_) => MessageType::DisproveReady, + GOATMessageContent::AssertReady(_) => MessageType::AssertReady, + GOATMessageContent::AssertSent(_) => MessageType::AssertSent, + GOATMessageContent::ChallengeAssertSent(_) => MessageType::ChallengeAssertSent, + GOATMessageContent::WronglyChallengeTimeout(_) => MessageType::WronglyChallengeTimeout, GOATMessageContent::DisproveSent(_) => MessageType::DisproveSent, GOATMessageContent::Take1Ready(_) => MessageType::Take1Ready, GOATMessageContent::Take1Sent(_) => MessageType::Take1Sent, diff --git a/node/src/scheduled_tasks/node_maintenance_tasks.rs b/node/src/scheduled_tasks/node_maintenance_tasks.rs index 293480f6..f49ea95d 100644 --- a/node/src/scheduled_tasks/node_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/node_maintenance_tasks.rs @@ -1,5 +1,5 @@ use alloy::primitives::Address; -use bitvm2_lib::actors::Actor; +use bitvm_lib::actors::Actor; use client::goat_chain::GOATClient; use std::str::FromStr; use store::localdb::{LocalDB, NodeQuery}; diff --git a/node/src/scheduled_tasks/sequencer_set_hash_monitor_task.rs b/node/src/scheduled_tasks/sequencer_set_hash_monitor_task.rs index c323a997..4231c5d6 100644 --- a/node/src/scheduled_tasks/sequencer_set_hash_monitor_task.rs +++ b/node/src/scheduled_tasks/sequencer_set_hash_monitor_task.rs @@ -293,21 +293,17 @@ mod tests { sync_sequencer_set_hash_changes(&db, &rpc, 10).await.unwrap(); - let mut s = db.acquire().await.unwrap(); // Should have 2 hash change records: one at block 10 (init), one at block 13 (change) + let mut s = db.acquire().await.unwrap(); let first = s - .find_first_sequencer_set_hash_change_by_cosmos_block_at_or_after(10) + .find_first_sequencer_set_hash_change_by_goat_block_at_or_before(1012) .await .unwrap() .unwrap(); assert_eq!(first.cosmos_block_height, 10); assert_eq!(first.validators_hash, hex::encode(make_hash(0xAA))); - let second = s - .find_first_sequencer_set_hash_change_by_cosmos_block_at_or_after(11) - .await - .unwrap() - .unwrap(); + let second = s.find_latest_sequencer_set_hash_change().await.unwrap().unwrap(); assert_eq!(second.cosmos_block_height, 13); assert_eq!(second.validators_hash, hex::encode(make_hash(0xBB))); } diff --git a/node/src/soldering_payload_store.rs b/node/src/soldering_payload_store.rs new file mode 100644 index 00000000..fae3edc6 --- /dev/null +++ b/node/src/soldering_payload_store.rs @@ -0,0 +1,175 @@ +use anyhow::{Context, Result, anyhow, bail}; +use aws_config::BehaviorVersion; +use aws_sdk_s3::primitives::ByteStream; +use std::path::{Path, PathBuf}; +use tokio::io::AsyncReadExt; +use uuid::Uuid; + +pub(crate) fn is_soldering_proof_s3_path(path: &str) -> bool { + path.trim().starts_with("s3://") +} + +pub(crate) fn soldering_proof_payload_store_path( + base_path: &str, + instance_id: Uuid, + graph_id: Uuid, + verifier_index: usize, + payload_hash: &[u8; 32], +) -> Result { + let base_path = base_path.trim(); + if base_path.is_empty() { + bail!("soldering proof payload store path cannot be empty"); + } + let payload_name = format!("{graph_id}-{verifier_index}-{}.bin", hex::encode(payload_hash)); + if is_soldering_proof_s3_path(base_path) { + Ok(format!("{}/{}/{}", base_path.trim_end_matches('/'), instance_id, payload_name)) + } else { + Ok(PathBuf::from(base_path) + .join(instance_id.to_string()) + .join(payload_name) + .to_string_lossy() + .to_string()) + } +} + +fn parse_soldering_proof_s3_path(path: &str) -> Result<(String, String)> { + let path = path.trim(); + let path = path + .strip_prefix("s3://") + .ok_or_else(|| anyhow!("soldering proof payload path is not an s3 path"))?; + let (bucket, key) = path + .split_once('/') + .ok_or_else(|| anyhow!("soldering proof s3 path must include bucket and key"))?; + if bucket.is_empty() { + bail!("soldering proof s3 path bucket cannot be empty"); + } + if key.is_empty() { + bail!("soldering proof s3 path key cannot be empty"); + } + Ok((bucket.to_string(), key.to_string())) +} + +pub(crate) async fn write_soldering_proof_store_payload(path: &str, payload: &[u8]) -> Result<()> { + if is_soldering_proof_s3_path(path) { + let (bucket, key) = parse_soldering_proof_s3_path(path)?; + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let client = aws_sdk_s3::Client::new(&config); + client + .put_object() + .bucket(&bucket) + .key(&key) + .body(ByteStream::from(payload.to_vec())) + .send() + .await + .with_context(|| format!("write soldering proof payload to {path}"))?; + return Ok(()); + } + + let path = Path::new(path.trim()); + if let Some(parent) = path.parent() { + tokio::fs::create_dir_all(parent) + .await + .with_context(|| format!("create soldering proof payload dir {}", parent.display()))?; + } + let mut temp_path = path.as_os_str().to_os_string(); + temp_path.push(".tmp"); + let temp_path = PathBuf::from(temp_path); + tokio::fs::write(&temp_path, payload) + .await + .with_context(|| format!("write soldering proof payload {}", temp_path.display()))?; + tokio::fs::rename(&temp_path, path).await.with_context(|| { + format!("move soldering proof payload {} to {}", temp_path.display(), path.display()) + })?; + Ok(()) +} + +pub(crate) async fn read_soldering_proof_store_payload(path: &str) -> Result> { + if is_soldering_proof_s3_path(path) { + let (bucket, key) = parse_soldering_proof_s3_path(path)?; + let config = aws_config::load_defaults(BehaviorVersion::latest()).await; + let client = aws_sdk_s3::Client::new(&config); + let object = client + .get_object() + .bucket(&bucket) + .key(&key) + .send() + .await + .with_context(|| format!("read soldering proof payload from {path}"))?; + let mut reader = object.body.into_async_read(); + let mut payload = Vec::new(); + reader + .read_to_end(&mut payload) + .await + .with_context(|| format!("read soldering proof s3 body from {path}"))?; + return Ok(payload); + } + + tokio::fs::read(path.trim()) + .await + .with_context(|| format!("read soldering proof payload {}", path.trim())) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn store_test_ids() -> (Uuid, Uuid, [u8; 32]) { + ( + Uuid::parse_str("11111111-1111-1111-1111-111111111111").unwrap(), + Uuid::parse_str("22222222-2222-2222-2222-222222222222").unwrap(), + [0xabu8; 32], + ) + } + + #[test] + fn soldering_payload_store_detects_s3_paths() { + assert!(is_soldering_proof_s3_path("s3://bucket/prefix")); + assert!(!is_soldering_proof_s3_path("/tmp/soldering")); + assert!(!is_soldering_proof_s3_path("relative/soldering")); + } + + #[test] + fn soldering_payload_store_builds_deterministic_paths() { + let (instance_id, graph_id, payload_hash) = store_test_ids(); + let s3_path = soldering_proof_payload_store_path( + "s3://bucket/prefix/", + instance_id, + graph_id, + 7, + &payload_hash, + ) + .unwrap(); + assert_eq!( + s3_path, + "s3://bucket/prefix/11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222-7-abababababababababababababababababababababababababababababababab.bin" + ); + + let local_path = soldering_proof_payload_store_path( + "/tmp/soldering/", + instance_id, + graph_id, + 7, + &payload_hash, + ) + .unwrap(); + assert_eq!( + local_path, + "/tmp/soldering/11111111-1111-1111-1111-111111111111/22222222-2222-2222-2222-222222222222-7-abababababababababababababababababababababababababababababababab.bin" + ); + } + + #[tokio::test] + async fn soldering_payload_store_local_write_read_round_trip() { + let (instance_id, graph_id, payload_hash) = store_test_ids(); + let temp_dir = tempfile::tempdir().unwrap(); + let base = temp_dir.path().to_string_lossy(); + let path = + soldering_proof_payload_store_path(&base, instance_id, graph_id, 0, &payload_hash) + .unwrap(); + let payload = b"compact soldering proof".to_vec(); + + write_soldering_proof_store_payload(&path, &payload).await.unwrap(); + let restored = read_soldering_proof_store_payload(&path).await.unwrap(); + assert_eq!(restored, payload); + } +} diff --git a/node/src/utils.rs b/node/src/utils.rs index 33dd76eb..9d9f6a7b 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -1,15 +1,20 @@ use crate::action::{ - GOATMessage, GOATMessageContent, NodeInfo, push_local_unhandled_messages, send_to_peer, + ChallengeSent, DisproveSent, GOATMessage, GOATMessageContent, KickoffSent, NodeInfo, + PreKickoffSent, SolderingProofReady, Take1Sent, Take2Sent, push_local_unhandled_messages, + send_to_peer, }; use crate::env::*; use crate::error::SpecialError; use crate::middleware::AllBehaviours; use crate::rpc_service::current_time_secs; -use crate::vk::get_vk; +use crate::soldering_payload_store::{ + is_soldering_proof_s3_path, soldering_proof_payload_store_path, + write_soldering_proof_store_payload, +}; use alloy::primitives::{Address as EvmAddress, Signature as EvmSignature}; use alloy::signers::Signer; use alloy::signers::local::PrivateKeySigner; -use anyhow::{Result, anyhow, bail}; +use anyhow::{Context, Result, anyhow, bail}; use bitcoin::address::NetworkUnchecked; use bitcoin::consensus::encode::{deserialize, serialize}; use bitcoin::hashes::Hash; @@ -19,42 +24,43 @@ use bitcoin::{ PrivateKey, PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey, }; -use bitcoin_light_client_circuit::{VK_HASH_SIZE, build_watchtower_commitment}; +use bitcoin_light_client_circuit::{ + VK_HASH_SIZE, build_watchtower_commitment, wrapper_public_values, +}; use bitvm::treepp::*; -use bitvm2_lib::actors::Actor; -use bitvm2_lib::challenger::*; -use bitvm2_lib::committee::*; -use bitvm2_lib::keys::{ChallengerMasterKey, OperatorMasterKey, WatchtowerMasterKey}; -use bitvm2_lib::operator::*; -use bitvm2_lib::types::{ - Bitvm2Graph, Bitvm2GraphParameters, Bitvm2InstanceParameters, Groth16Proof, GuestInputs, - PrekickoffParameters, PublicInputs, SimplifiedBitvm2Graph, UserInfo, VerifyingKey, +use bitvm_lib::actors::Actor; +use bitvm_lib::committee::*; +use bitvm_lib::keys::{OperatorMasterKey, VerifierMasterKey, WatchtowerMasterKey}; +use bitvm_lib::operator::*; +use bitvm_lib::types::{ + BitvmGcCircuitData, BitvmGcGraph, BitvmGcGraphParameters, BitvmGcInstanceParameters, + PrekickoffParameters, SimplifiedBitvmGcGraph, UserInfo, }; -use bitvm2_lib::watchtower::*; +use bitvm_lib::verifier::*; +use bitvm_lib::watchtower::*; use client::Utxo as ClientUtxo; use client::{btc_chain::BTCClient, goat_chain::GOATClient}; use esplora_client::Utxo; -use futures::future::try_join_all; use goat::connectors::{ base::TaprootConnector, kickoff_connectors::{ForceSkipConnector, KickoffConnector, PrekickoffConnector}, }; use goat::contexts::base::generate_n_of_n_public_key; -use goat::disprove_scripts::hash160; -use goat::scripts::generate_opreturn_script; -use goat::transactions::base::Input; +use goat::scripts::{generate_opreturn_script, p2a_output}; +use goat::transactions::base::{Input, output_topology}; use goat::transactions::pre_signed::PreSignedTransaction; use goat::transactions::prekickoff::PrekickoffTransaction; use goat::transactions::signing::populate_p2wsh_witness; use indexmap::IndexMap; use libp2p::{PeerId, Swarm}; use musig2::{PartialSignature, PubNonce}; +use p3_bn254_fr::Bn254Fr; +use p3_field::{FieldAlgebra, PrimeField}; use rand::Rng; use reqwest::Url; use secp256k1::Secp256k1; use serde::{Deserialize, Serialize}; -use std::fs::{self, File}; -use std::io::{BufReader, BufWriter}; +use sha2::{Digest, Sha256}; use std::net::SocketAddr; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::{Path, PathBuf}; @@ -65,47 +71,74 @@ use std::time::{SystemTime, UNIX_EPOCH}; use store::localdb::{ GraphQuery, GraphUpdate, InstanceQuery, InstanceUpdate, LocalDB, StorageProcessor, }; -use store::{ - BridgeOutGlobalStats, ByteArray32, Graph, GraphRawData, GraphStatus, Instance, - InstanceBridgeInStatus, Message, MessageState, MessageType, Node, PeginGraphProcessData, - PeginInstanceProcessData, SerializableTxid, UInt64Array3, -}; -use stun_client::{Attribute, Class, Client}; -use zkm_sdk::ZKMProofWithPublicValues; -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk}; use crate::env; use crate::rpc_service::routes::v1::{ - NODES_OPERATOR_BASE, NODES_WATCHTOWER_BASE, PROOFS_OPERATOR_PROOF_TIMEOUT, - PROOFS_WATCHTOWER_PROOF_TIMEOUT, + NODES_OPERATOR_BASE, NODES_WATCHTOWER_BASE, PROOFS_WATCHTOWER_PROOF_TIMEOUT, + PROOFS_WRAPPER_PROOF, }; use crate::scheduled_tasks::get_goat_message_content_type; use crate::scheduled_tasks::graph_maintenance_tasks::{ - AssertCommitItemStatus, AssertCommitStatus, ChallengeSubStatus, CommitBlockHashStatus, - WatchtowerChallengeItemStatus, WatchtowerChallengeStatus, refresh_assert_monitor_data, - refresh_watchtower_challenge_monitor_data, + ChallengeSubStatus, VerifierChallengeStatus, }; use bitcoin_light_client_circuit::hash_operator_constant; -use bitvm2_lib::transactions::base::BaseTransaction; +use bitvm_lib::babe_adapter::{ + BabeProverState, BabeVerifierPrivateState, CACSetupPackage, FinalizedInstanceData, + SolderingData, compact_soldering_proof_payload, +}; +use bitvm_lib::transactions::base::BaseTransaction; use client::goat_chain::{DisproveTxType, GraphData, PeginStatus, WithdrawStatus}; use client::http_client::async_client::HttpAsyncClient; use proof_builder::{ - OperatorProofRequest, OperatorProofResponse, OperatorProofTimeoutUpdateRequest, - OperatorProofTimeoutUpdateResponse, ProofData, WatchtowerProofRequest, WatchtowerProofResponse, - WatchtowerProofTimeoutUpdateRequest, WatchtowerProofTimeoutUpdateResponse, + OperatorProofRequest, OperatorProofResponse, ProofData, WatchtowerProofRequest, + WatchtowerProofResponse, WatchtowerProofTimeoutUpdateRequest, + WatchtowerProofTimeoutUpdateResponse, WrapperProofResponse, +}; +use store::{ + BridgeOutGlobalStats, ByteArray32, Graph, GraphRawData, GraphStatus, Instance, + InstanceBridgeInStatus, Message, MessageState, MessageType, Node, PeginGraphProcessData, + PeginInstanceProcessData, SerializableTxid, UInt64Array3, }; +use stun_client::{Attribute, Class, Client}; use tracing::{error, info, warn}; use uuid::Uuid; +use zkm_recursion_core::stark::KoalaBearPoseidon2Outer; +use zkm_sdk::ZKMProofWithPublicValues; +use zkm_stark::PartStarkVerifyingKey; +use zkm_verifier::{ + Groth16Verifier, IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk, decode_zkm_vkey_hash, + hash_public_inputs, load_ark_public_inputs_from_bytes, +}; + pub(crate) const BRIDGE_OUT_GLOBAL_STATS_ID: i64 = 1; + +pub type VerifyingKey = ark_groth16::VerifyingKey; +pub type Groth16Proof = ark_groth16::Proof; +pub type PublicInputs = Vec; + +#[derive(Clone)] +pub struct ValidatedOperatorWrapperProof { + pub proof: Groth16Proof, + pub public_inputs: PublicInputs, + pub verifying_key: VerifyingKey, + pub public_values: Vec, + pub wrapper_vk_hash: String, + pub zkm_version: String, +} + +#[derive(Clone)] +pub struct OperatorWrapperStatement { + pub public_values: Vec, + pub public_inputs: PublicInputs, + pub wrapper_vk_hash: String, + pub zkm_version: String, +} + pub mod todo_funcs { #![allow(dead_code, unreachable_code, unused_variables)] use super::*; - use bitvm::chunk::api::{NUM_HASH, NUM_PUBS, NUM_U256}; - use bitvm2_lib::types::SimplifiedBitvm2Graph; - use goat::{ - connectors::assert_connectors::chunk_assert_commit, disprove_scripts::NUM_GUEST_PUBS_ASSERT, - }; + use bitvm_lib::types::SimplifiedBitvmGcGraph; // other operations pub fn avg_block_time_secs(network: Network) -> u64 { @@ -118,12 +151,6 @@ pub mod todo_funcs { // _ => 600, // default to 10 minutes } } - pub fn assert_commmit_num() -> usize { - let use_compact = false; - let wots32_num = NUM_GUEST_PUBS_ASSERT + NUM_PUBS + NUM_U256; - let wots16_num = NUM_HASH; - chunk_assert_commit(wots32_num, wots16_num, use_compact).len() - } pub fn min_required_operator() -> usize { // todo!("get min required operator number") 1 @@ -132,16 +159,21 @@ pub mod todo_funcs { // todo!("get min required watchtower number") 1 } + pub fn min_required_verifier() -> usize { + // todo!("get verifier num") + 1 + } + pub async fn validate_init_graph( local_db: &LocalDB, btc_client: &BTCClient, goat_client: &GOATClient, - graph: &SimplifiedBitvm2Graph, + graph: &SimplifiedBitvmGcGraph, ) -> Result<()> { // Basic structural and on-chain consistency checks for an incoming graph proposal. // Return SpecialError::InvalidGraph on any validation failure. // 1) Rebuild full graph (ensures signatures present if flags are set and tx graph is coherent) - let full_graph = Bitvm2Graph::from_simplified(graph) + let full_graph = BitvmGcGraph::from_simplified(graph) .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; // 2) Network must match local node network @@ -169,20 +201,13 @@ pub mod todo_funcs { if graph.parameters.challenge_amount != super::todo_funcs::challenge_amount() { bail!(SpecialError::InvalidGraph("unexpected challenge amount".to_string())); } - if graph.assert_commit_num != super::todo_funcs::assert_commmit_num() { - bail!(SpecialError::InvalidGraph("unexpected assert_commit_num".to_string())); - } // 5) Watchtower config sanity: number of watchtowers should match number of hashlocks and registry size let watchtowers_on_chain = goat_client.committee_mana_get_watchtowers().await.map_err(|e| { SpecialError::InvalidGraph(format!("failed to load watchtowers from chain: {e}")) })?; - if graph.parameters.watchtower_pubkeys.len() != graph.parameters.hashlocks.len() { - bail!(SpecialError::InvalidGraph( - "watchtower_pubkeys and hashlocks length mismatch".to_string() - )); - } + // deduplicate watchtower pubkeys: reject graphs that contain duplicate watchtower entries { use std::collections::HashSet; @@ -238,11 +263,11 @@ pub mod todo_funcs { } pub async fn validate_finalized_graph( goat_client: &GOATClient, - graph: &SimplifiedBitvm2Graph, + graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, EvmAddress, Vec)], ) -> Result<()> { // 1) Rebuild full graph to ensure structure is coherent and txns derivable - let full_graph = Bitvm2Graph::from_simplified(graph) + let full_graph = BitvmGcGraph::from_simplified(graph) .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; // 2) Repeat key static checks (network, committee set, counts) @@ -263,17 +288,9 @@ pub mod todo_funcs { "committee pubkeys mismatch with GoatChain".to_string() )); } - if graph.parameters.watchtower_pubkeys.len() != graph.parameters.hashlocks.len() { - bail!(SpecialError::InvalidGraph( - "watchtower_pubkeys and hashlocks length mismatch".to_string() - )); - } if graph.parameters.challenge_amount != super::todo_funcs::challenge_amount() { bail!(SpecialError::InvalidGraph("unexpected challenge amount".to_string())); } - if graph.assert_commit_num != super::todo_funcs::assert_commmit_num() { - bail!(SpecialError::InvalidGraph("unexpected assert_commit_num".to_string())); - } // 3) Validate endorsements: unique, from legitimate committee members, and signatures recover to the provided EVM address use std::collections::HashSet; @@ -337,7 +354,7 @@ pub mod todo_funcs { Amount::from_sat(500000) } pub fn min_prekickoff_input_amount() -> Amount { - Amount::from_sat(100000) + Amount::from_sat(200000) } pub fn challenge_amount() -> Amount { Amount::from_sat(20000) @@ -347,15 +364,6 @@ pub mod todo_funcs { + (replenish_fee_inputs_num as u64 * CHEKSIG_P2WSH_INPUT_VBYTES); Amount::from_sat(tx_vbytes) } - pub async fn get_preimage( - local_db: &LocalDB, - instance_id: Uuid, - graph_id: Uuid, - index: usize, - ) -> Result> { - let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); - Ok(operator_master_key.preimage_for_graph(graph_id, index)) - } } pub mod evm_swap_utils { @@ -772,31 +780,215 @@ pub(crate) async fn refresh_graph( goat_client: &GOATClient, instance_id: Uuid, graph_id: Uuid, - graph: Option<&Bitvm2Graph>, + graph: Option<&BitvmGcGraph>, scan_from_status: Option, scan_from_sub_status: Option, -) -> Result<(GraphStatus, Option)> { - let graph = match graph { - Some(g) => g, - None => { - let g = get_graph(local_db, instance_id, graph_id).await?; - match g { - Some(g) => &Bitvm2Graph::from_simplified(&g)?, - None => bail!("Graph {graph_id} not found in local db"), - } - } +) -> Result<(GraphStatus, Option, Option)> { + let Some(graph) = graph else { + let status = scan_from_status.unwrap_or(GraphStatus::OperatorPresigned); + return Ok((status, scan_from_sub_status, None)); + }; + + let scan = scan_graph_chain_state( + btc_client, + goat_client, + graph, + scan_from_status, + scan_from_sub_status, + ) + .await?; + + if let Some(challenge_txid) = scan.challenge_txid { + update_graph_challenge_txid_if_needed(local_db, graph_id, challenge_txid).await?; + } + + update_graph_status( + local_db, + instance_id, + graph_id, + scan.status, + Some(scan.sub_status.clone()), + ) + .await?; + + Ok((scan.status, Some(scan.sub_status.clone()), Some(scan))) +} + +#[derive(Clone, Debug)] +struct DetectedDisprove { + disprove_type: DisproveTxType, + index: usize, + challenge_start_txid: Option, + challenge_finish_txid: Txid, +} + +#[derive(Clone, Debug)] +pub(crate) struct GraphChainScan { + status: GraphStatus, + sub_status: ChallengeSubStatus, + challenge_txid: Option, + #[allow(dead_code)] + watchtower_challenge_init_on_chain: bool, + #[allow(dead_code)] + operator_assert_on_chain: bool, + disprove: Option, +} + +fn normalize_challenge_sub_status( + mut sub_status: ChallengeSubStatus, + watchtower_num: usize, + verifier_num: usize, +) -> ChallengeSubStatus { + sub_status.watchtower_challenge_status.resize(watchtower_num, false); + sub_status.verifier_challenge_status.resize(verifier_num, VerifierChallengeStatus::None); + sub_status +} + +fn connector_a_outpoint(graph: &BitvmGcGraph) -> Result { + graph + .kickoff + .connector_a_input() + .map(|input| input.outpoint) + .map_err(|e| anyhow!("failed to get connector-a input: {e}")) +} + +fn connector_d_outpoint(graph: &BitvmGcGraph) -> Result { + graph + .operator_assert + .connector_d_input() + .map(|input| input.outpoint) + .map_err(|e| anyhow!("failed to get connector-d input: {e}")) +} + +#[allow(dead_code)] +fn guardian_connector_outpoint(graph: &BitvmGcGraph) -> Result { + graph + .kickoff + .guardian_connector_input() + .map(|input| input.outpoint) + .map_err(|e| anyhow!("failed to get guardian connector input: {e}")) +} + +async fn update_graph_challenge_txid_if_needed( + local_db: &LocalDB, + graph_id: Uuid, + challenge_txid: Txid, +) -> Result<()> { + let mut storage_processor = local_db.acquire().await?; + let Some(graph) = storage_processor.find_graph(&graph_id).await? else { + warn!("graph: {graph_id} not found, skip updating challenge txid"); + return Ok(()); + }; + if graph.challenge_txid.as_ref().map(|txid| txid.0) != Some(challenge_txid) { + storage_processor + .update_graph(&GraphUpdate::new(graph_id).with_challenge_txid(challenge_txid.into())) + .await?; + } + Ok(()) +} + +#[allow(dead_code)] +async fn detect_guardian_disprove( + btc_client: &BTCClient, + graph: &BitvmGcGraph, + challenge_start_txid: Option, +) -> Result> { + let quick_challenge_txid = graph.quick_challenge.tx().compute_txid(); + if tx_on_chain(btc_client, &quick_challenge_txid).await? { + return Ok(Some(DetectedDisprove { + disprove_type: DisproveTxType::QuickChallenge, + index: 0, + challenge_start_txid, + challenge_finish_txid: quick_challenge_txid, + })); + } + + let challenge_incomplete_kickoff_txid = graph.challenge_incomplete_kickoff.tx().compute_txid(); + if tx_on_chain(btc_client, &challenge_incomplete_kickoff_txid).await? { + return Ok(Some(DetectedDisprove { + disprove_type: DisproveTxType::ChallengeIncompleteKickoff, + index: 0, + challenge_start_txid, + challenge_finish_txid: challenge_incomplete_kickoff_txid, + })); + } + Ok(None) +} + +async fn detect_connector_d_disprove( + btc_client: &BTCClient, + graph: &BitvmGcGraph, + challenge_start_txid: Option, +) -> Result> { + let connector_d = connector_d_outpoint(graph)?; + let Some(spent_txid) = + outpoint_spent_txid(btc_client, &connector_d.txid, connector_d.vout as u64).await? + else { + return Ok(None); }; + + if spent_txid == graph.take2.tx().compute_txid() { + return Ok(None); + } + + for (index, disprove) in graph.disproves.iter().enumerate() { + if spent_txid == disprove.tx().compute_txid() { + return Ok(Some(DetectedDisprove { + disprove_type: DisproveTxType::Disprove, + index, + challenge_start_txid, + challenge_finish_txid: spent_txid, + })); + } + } + + Ok(Some(DetectedDisprove { + disprove_type: DisproveTxType::PubinDisprove, + index: 0, + challenge_start_txid: None, + challenge_finish_txid: spent_txid, + })) +} + +async fn scan_graph_chain_state( + btc_client: &BTCClient, + goat_client: &GOATClient, + graph: &BitvmGcGraph, + scan_from_status: Option, + scan_from_sub_status: Option, +) -> Result { + let instance_id = graph.parameters.instance_parameters.instance_id; + let graph_id = graph.parameters.graph_id; + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + let verifier_num = graph.verifier_asserts.len(); + let mut sub_status = normalize_challenge_sub_status( + scan_from_sub_status.unwrap_or_default(), + watchtower_num, + verifier_num, + ); let mut current_status = match scan_from_status { Some(s) => s, None => { if graph.committee_pre_signed() { GraphStatus::CommitteePresigned } else { - return Ok((GraphStatus::OperatorPresigned, None)); + return Ok(GraphChainScan { + status: GraphStatus::OperatorPresigned, + sub_status, + challenge_txid: None, + watchtower_challenge_init_on_chain: false, + operator_assert_on_chain: false, + disprove: None, + }); } } }; - let mut sub_status = scan_from_sub_status.unwrap_or_default(); + + let prekickoff_txid = graph.cur_prekickoff.tx().compute_txid(); + let kickoff_txid = graph.kickoff.tx().compute_txid(); + let take1_txid = graph.take1.tx().compute_txid(); + let take2_txid = graph.take2.tx().compute_txid(); + // check if Graph has been posted on GoatChain if current_status == GraphStatus::CommitteePresigned { let graph_data_on_goat = goat_client.gateway_get_graph_data(&graph_id).await?; @@ -816,7 +1008,6 @@ pub(crate) async fn refresh_graph( } } // check Prekickoff - let prekickoff_txid = graph.cur_prekickoff.tx().compute_txid(); if matches!( current_status, GraphStatus::OperatorPresigned @@ -825,8 +1016,14 @@ pub(crate) async fn refresh_graph( | GraphStatus::Obsoleted ) { if !tx_on_chain(btc_client, &prekickoff_txid).await? { - update_graph_status(local_db, instance_id, graph_id, current_status, None).await?; - return Ok((current_status, None)); + return Ok(GraphChainScan { + status: current_status, + sub_status, + challenge_txid: None, + watchtower_challenge_init_on_chain: false, + operator_assert_on_chain: false, + disprove: None, + }); } else { current_status = if current_status == GraphStatus::OperatorDataPushed { GraphStatus::PreKickoff @@ -843,264 +1040,243 @@ pub(crate) async fn refresh_graph( } } // check Kickoff/SkipKickoff - let kickoff_txid = graph.kickoff.tx().compute_txid(); if matches!(current_status, GraphStatus::PreKickoff | GraphStatus::Obsoleted) { let kickoff_connector_vout = 1; if let Some(spent_txid) = outpoint_spent_txid(btc_client, &prekickoff_txid, kickoff_connector_vout).await? { if spent_txid != kickoff_txid { - update_graph_status(local_db, instance_id, graph_id, GraphStatus::Skipped, None) - .await?; - return Ok((GraphStatus::Skipped, None)); + return Ok(GraphChainScan { + status: GraphStatus::Skipped, + sub_status, + challenge_txid: None, + watchtower_challenge_init_on_chain: false, + operator_assert_on_chain: false, + disprove: None, + }); } else { current_status = GraphStatus::OperatorKickOff; } } else { - update_graph_status(local_db, instance_id, graph_id, current_status, None).await?; - return Ok((current_status, None)); + return Ok(GraphChainScan { + status: current_status, + sub_status, + challenge_txid: None, + watchtower_challenge_init_on_chain: false, + operator_assert_on_chain: false, + disprove: None, + }); } } - // check Take1/Challenge - let take1_txid = graph.take1.tx().compute_txid(); - let connector_a_vout = 0; + + let mut challenge_txid = None; + if current_status == GraphStatus::OperatorKickOff + && let Some(disprove) = detect_guardian_disprove(btc_client, graph, challenge_txid).await? + { + sub_status.disprove_type = Some(disprove.disprove_type); + sub_status.disprove_index = disprove.index as i32; + return Ok(GraphChainScan { + status: GraphStatus::Disprove, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain: false, + operator_assert_on_chain: false, + disprove: Some(disprove), + }); + } if current_status == GraphStatus::OperatorKickOff { + let connector_a = connector_a_outpoint(graph)?; if let Some(spent_txid) = - outpoint_spent_txid(btc_client, &kickoff_txid, connector_a_vout).await? + outpoint_spent_txid(btc_client, &connector_a.txid, connector_a.vout as u64).await? { if spent_txid != take1_txid { current_status = GraphStatus::Challenge; + challenge_txid = Some(spent_txid); } else { - update_graph_status( - local_db, - instance_id, - graph_id, - GraphStatus::OperatorTake1, - None, - ) - .await?; - return Ok((GraphStatus::OperatorTake1, None)); + return Ok(GraphChainScan { + status: GraphStatus::OperatorTake1, + sub_status, + challenge_txid: None, + watchtower_challenge_init_on_chain: false, + operator_assert_on_chain: false, + disprove: None, + }); } } else { - update_graph_status( - local_db, - instance_id, - graph_id, - GraphStatus::OperatorKickOff, - None, - ) - .await?; - return Ok((GraphStatus::OperatorKickOff, None)); + return Ok(GraphChainScan { + status: GraphStatus::OperatorKickOff, + sub_status, + challenge_txid: None, + watchtower_challenge_init_on_chain: false, + operator_assert_on_chain: false, + disprove: None, + }); } } - try_update_graph_challenge_txid( - btc_client, - local_db, - graph_id, - kickoff_txid, - connector_a_vout, - take1_txid, - ) - .await?; - // check Take2/Disprove - let take2_txid = graph.take2.tx().compute_txid(); - if current_status == GraphStatus::Challenge { - let connector_e_vout = 3; + + if current_status == GraphStatus::Challenge && challenge_txid.is_none() { + let connector_a = connector_a_outpoint(graph)?; if let Some(spent_txid) = - outpoint_spent_txid(btc_client, &kickoff_txid, connector_e_vout).await? + outpoint_spent_txid(btc_client, &connector_a.txid, connector_a.vout as u64).await? + && spent_txid != take1_txid { - let (current_status, sub_status) = if spent_txid != take2_txid { - sub_status.disprove_type = Some(DisproveTxType::Disprove); - (GraphStatus::Disprove, Some(sub_status)) - } else { - (GraphStatus::OperatorTake2, None) - }; - update_graph_status(local_db, instance_id, graph_id, current_status, sub_status) - .await?; - return Ok((current_status, sub_status)); + challenge_txid = Some(spent_txid); } } - // check Watchtower-Challenge & Assert-Commit process + + let mut watchtower_challenge_init_on_chain = false; + let mut operator_assert_on_chain = false; if current_status == GraphStatus::Challenge { - let network = get_network(); - let current_height = btc_client.get_height().await? as i64; - let db_graph = convert_graph(graph, current_time_secs()); - // check Watchtower Challenge process - let (vout_monitor_data, watchtower_challenge_init_height, _) = - match refresh_watchtower_challenge_monitor_data(local_db, btc_client, &db_graph).await? - { - Some(data) => data, - None => { - update_graph_status( - local_db, - instance_id, - graph_id, - current_status, - Some(sub_status), - ) - .await?; - return Ok((current_status, Some(sub_status))); - } - }; - let is_challenge_timeout = watchtower_challenge_init_height - + (watchtower_challenge_timeout_timelock(network) as i64) - < current_height; - let is_ack_timeout = - watchtower_challenge_init_height + (nack_timelock(network) as i64) < current_height; - let is_blockhash_commit_timeout = watchtower_challenge_init_height - + (commit_blockhash_timeout_timelock(network) as i64) - < current_height; - // 1) check watchtower challenge status - if let Some((&index, _)) = vout_monitor_data - .data_map - .iter() - .find(|(_, status)| **status == WatchtowerChallengeItemStatus::OperatorNACK) - { - // 1.1) WatchtowerChallengeDisproveFinished - sub_status.watchtower_challenge_status = - WatchtowerChallengeStatus::WatchtowerChallengeDisproveFinished; - sub_status.disprove_index = index; - sub_status.disprove_type = Some(DisproveTxType::OperatorNack); - current_status = GraphStatus::Disprove; - update_graph_status(local_db, instance_id, graph_id, current_status, Some(sub_status)) - .await?; - // no further check is needed if Disproved - return Ok((current_status, Some(sub_status))); - } else if vout_monitor_data.data_map.values().all(|status| { - matches!( - status, - WatchtowerChallengeItemStatus::OperatorACK - | WatchtowerChallengeItemStatus::ChallengeTimeout - ) - }) { - // 1.2) WatchtowerChallengeNormalFinished - sub_status.watchtower_challenge_status = - WatchtowerChallengeStatus::WatchtowerChallengeNormalFinished; - } else if vout_monitor_data.data_map.values().any(|status| { - matches!( - status, - WatchtowerChallengeItemStatus::OperatorInit - | WatchtowerChallengeItemStatus::Challenge - ) - }) && is_ack_timeout - { - // 1.3) OperatorACKTimeout - sub_status.watchtower_challenge_status = WatchtowerChallengeStatus::OperatorACKTimeout; - } else if vout_monitor_data - .data_map - .values() - .any(|status| *status == WatchtowerChallengeItemStatus::OperatorInit) - && is_challenge_timeout - { - // 1.4) WatchtowerChallengeTimeout - sub_status.watchtower_challenge_status = - WatchtowerChallengeStatus::WatchtowerChallengeTimeout; - } else if vout_monitor_data - .data_map - .values() - .any(|status| *status == WatchtowerChallengeItemStatus::Challenge) - { - // 1.5) WatchtowerChallenge - sub_status.watchtower_challenge_status = WatchtowerChallengeStatus::WatchtowerChallenge; - } else if vout_monitor_data - .data_map - .values() - .any(|status| *status == WatchtowerChallengeItemStatus::OperatorInit) - { - // 1.6) OperatorInit - sub_status.watchtower_challenge_status = WatchtowerChallengeStatus::OperatorInit; - } else { - // 1.7) None - sub_status.watchtower_challenge_status = WatchtowerChallengeStatus::None; + if let Some(disprove) = detect_guardian_disprove(btc_client, graph, challenge_txid).await? { + sub_status.disprove_type = Some(disprove.disprove_type); + sub_status.disprove_index = disprove.index as i32; + return Ok(GraphChainScan { + status: GraphStatus::Disprove, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: Some(disprove), + }); } - // 2) check commit blockhash status - if vout_monitor_data.commit_blockhash_status == CommitBlockHashStatus::OperatorCommitTimeout - { - // 2.1) Disproved by OperatorCommitTimeout - sub_status.commit_blockhash_status = CommitBlockHashStatus::OperatorCommitTimeout; - sub_status.disprove_index = 0; - sub_status.disprove_type = Some(DisproveTxType::OperatorCommitTimeout); - current_status = GraphStatus::Disprove; - update_graph_status(local_db, instance_id, graph_id, current_status, Some(sub_status)) - .await?; - return Ok((current_status, Some(sub_status))); - } else if vout_monitor_data.commit_blockhash_status == CommitBlockHashStatus::OperatorCommit - { - // 2.2) OperatorCommit - sub_status.commit_blockhash_status = CommitBlockHashStatus::OperatorCommit; - } else if sub_status.watchtower_challenge_status - == WatchtowerChallengeStatus::WatchtowerChallengeNormalFinished - { - if is_blockhash_commit_timeout { - // 2.3) WatchtowerChallengeCommitTimeout - sub_status.commit_blockhash_status = CommitBlockHashStatus::OperatorCommitTimeout; - } else { - // 2.4) WatchtowerChallengeProcessed - sub_status.commit_blockhash_status = - CommitBlockHashStatus::WatchtowerChallengeProcessed; + + let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); + watchtower_challenge_init_on_chain = + tx_on_chain(btc_client, &watchtower_challenge_init_txid).await?; + if watchtower_challenge_init_on_chain { + for watchtower_index in 0..watchtower_num { + let watchtower_vout = + output_topology::watchtower_challenge_init::watchtower_connector( + watchtower_index, + ) as u64; + let spent = outpoint_spent_txid( + btc_client, + &watchtower_challenge_init_txid, + watchtower_vout, + ) + .await? + .is_some(); + if let Some(status) = + sub_status.watchtower_challenge_status.get_mut(watchtower_index) + { + *status = spent; + } } - } else { - // 2.5) None - sub_status.commit_blockhash_status = CommitBlockHashStatus::None; } - // 3) check Assert Commit process - let (vout_monitor_data, assert_init_height, _) = - match refresh_assert_monitor_data(local_db, btc_client, &db_graph).await? { - Some(data) => data, - None => { - update_graph_status( - local_db, - instance_id, - graph_id, - current_status, - Some(sub_status), - ) - .await?; - return Ok((current_status, Some(sub_status))); + if !watchtower_challenge_init_on_chain { + return Ok(GraphChainScan { + status: current_status, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain: false, + disprove: None, + }); + } + let operator_assert_txid = graph.operator_assert.tx().compute_txid(); + operator_assert_on_chain = tx_on_chain(btc_client, &operator_assert_txid).await?; + if !operator_assert_on_chain { + return Ok(GraphChainScan { + status: current_status, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: None, + }); + } + // TBD: add GraphStatus::Assert + } + + if current_status == GraphStatus::Challenge { + if let Some(disprove) = detect_guardian_disprove(btc_client, graph, challenge_txid).await? { + sub_status.disprove_type = Some(disprove.disprove_type); + sub_status.disprove_index = disprove.index as i32; + return Ok(GraphChainScan { + status: GraphStatus::Disprove, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: Some(disprove), + }); + } + + // TBD: add GraphStatus::Assert + let mut detected_disprove = None; + for verifier_index in 0..verifier_num { + let verifier_assert_txid = graph.verifier_asserts[verifier_index].tx().compute_txid(); + let disprove_txid = graph.disproves[verifier_index].tx().compute_txid(); + if tx_on_chain(btc_client, &verifier_assert_txid).await? { + if let Some(spent_txid) = + outpoint_spent_txid(btc_client, &verifier_assert_txid, 0).await? + { + if spent_txid == disprove_txid { + sub_status.verifier_challenge_status[verifier_index] = + VerifierChallengeStatus::Disproved; + sub_status.disprove_type = Some(DisproveTxType::Disprove); + sub_status.disprove_index = verifier_index as i32; + detected_disprove = Some(DetectedDisprove { + disprove_type: DisproveTxType::Disprove, + index: verifier_index, + challenge_start_txid: challenge_txid, + challenge_finish_txid: spent_txid, + }); + current_status = GraphStatus::Disprove; + } else { + sub_status.verifier_challenge_status[verifier_index] = + VerifierChallengeStatus::ProverAnswered; + } + } else { + sub_status.verifier_challenge_status[verifier_index] = + VerifierChallengeStatus::VerifierAsserted; } - }; - let is_assert_commit_timeout = - assert_init_height + (assert_commit_timeout_timelock(network) as i64) < current_height; - if let Some((&index, _)) = vout_monitor_data - .data_map - .iter() - .find(|(_, status)| **status == AssertCommitItemStatus::OperatorCommitTimeout) - { - // 3.1) Disproved by OperatorCommitTimeout - sub_status.assert_commit_status = AssertCommitStatus::OperatorCommitTimeout; - sub_status.disprove_index = index; - sub_status.disprove_type = Some(DisproveTxType::AssertTimeout); - current_status = GraphStatus::Disprove; - update_graph_status(local_db, instance_id, graph_id, current_status, Some(sub_status)) - .await?; - // no further check is needed if Disproved - return Ok((current_status, Some(sub_status))); - } else if vout_monitor_data - .data_map - .values() - .all(|status| *status == AssertCommitItemStatus::OperatorCommit) - { - // 3.2) OperatorCommit - sub_status.assert_commit_status = AssertCommitStatus::OperatorCommit; - } else if vout_monitor_data - .data_map - .values() - .any(|status| *status == AssertCommitItemStatus::OperatorInit) - { - if is_assert_commit_timeout { - // 3.3) AssertCommitTimeout - sub_status.assert_commit_status = AssertCommitStatus::OperatorCommitTimeout; - } else { - // 3.4) OperatorInit - sub_status.assert_commit_status = AssertCommitStatus::OperatorInit; } - } else { - // 3.5) None - sub_status.assert_commit_status = AssertCommitStatus::None; + } + if current_status == GraphStatus::Disprove { + return Ok(GraphChainScan { + status: current_status, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: detected_disprove, + }); + } + if let Some(disprove) = + detect_connector_d_disprove(btc_client, graph, challenge_txid).await? + { + sub_status.disprove_type = Some(disprove.disprove_type); + sub_status.disprove_index = disprove.index as i32; + return Ok(GraphChainScan { + status: GraphStatus::Disprove, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: Some(disprove), + }); + } + if tx_on_chain(btc_client, &take2_txid).await? { + return Ok(GraphChainScan { + status: GraphStatus::OperatorTake2, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: None, + }); } } - update_graph_status(local_db, instance_id, graph_id, current_status, Some(sub_status)).await?; - Ok((current_status, Some(sub_status))) + + Ok(GraphChainScan { + status: current_status, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: None, + }) } #[allow(clippy::enum_variant_names)] @@ -1122,238 +1298,184 @@ fn map_transition_to_event(from: GraphStatus, to: GraphStatus) -> Option Some(GraphCompensateEventKind::Take1Sent), (OperatorKickOff, Challenge) => Some(GraphCompensateEventKind::ChallengeSent), (Challenge, Disprove) => Some(GraphCompensateEventKind::DisproveSent), + (OperatorKickOff, Disprove) => Some(GraphCompensateEventKind::DisproveSent), (Challenge, OperatorTake2) => Some(GraphCompensateEventKind::Take2Sent), _ => None, } } +async fn upsert_graph_compensate_message( + local_db: &LocalDB, + graph_id: Uuid, + sub_type: Option, + actor: Actor, + message_content: GOATMessageContent, +) -> Result<()> { + let mut storage_processor = local_db.acquire().await?; + upsert_message( + &mut storage_processor, + false, + graph_id, + sub_type, + SELF_SENDER.to_string(), + actor, + message_content, + 0, + 0, + ) + .await +} + +async fn push_graph_compensate_message( + local_db: &LocalDB, + graph_id: Uuid, + actor: Actor, + message_content: GOATMessageContent, +) -> Result<()> { + let message = GOATMessage::new(actor, message_content); + push_local_unhandled_messages(local_db, graph_id, &message, 0).await +} + +#[allow(dead_code)] +async fn should_emit_wrongly_challenge_timeout( + btc_client: &BTCClient, + challenge_assert_txid: Txid, +) -> Result { + let status = btc_client.get_tx_status(&challenge_assert_txid).await?; + let Some(block_height) = status.block_height else { + return Ok(false); + }; + let current_height = btc_client.get_height().await?; + Ok(current_height >= block_height + disprove_timelock(get_network())) +} + #[allow(clippy::too_many_arguments)] pub(crate) async fn compensate_graph_events( local_db: &LocalDB, - btc_client: &BTCClient, + _btc_client: &BTCClient, instance_id: Uuid, graph_id: Uuid, - graph: Option<&Bitvm2Graph>, + _graph: Option<&BitvmGcGraph>, + scan: Option<&GraphChainScan>, scan_from_status: Option, compensate_from_status: GraphStatus, final_status: GraphStatus, - final_sub_status: Option, ) -> Result<()> { - use GOATMessageContent::*; + let Some(scan) = scan else { + tracing::debug!( + "Skip graph compensation for {instance_id}:{graph_id}: chain scan result is missing" + ); + return Ok(()); + }; let scan_start = scan_from_status.unwrap_or(compensate_from_status); - let effective_from = if scan_start.is_after(&compensate_from_status) { scan_start } else { compensate_from_status }; - if !effective_from.is_before(&final_status) { tracing::debug!( - "Skip compensating graph events: effective_from {effective_from:?} is not before final_status {final_status:?}", + "Skip graph compensation for {instance_id}:{graph_id}: effective_from={effective_from:?}, final_status={final_status:?}" ); return Ok(()); } - let mut rev_path = Vec::new(); - let mut cur = final_status; - loop { - rev_path.push(cur); - if cur == effective_from { - break; - } - cur = match cur.get_previous_status() { - Some(prev) => prev, - None => { - tracing::debug!( - "Stop compensating graph events early: no previous status for {cur:?} while targeting {effective_from:?}", - ); - return Ok(()); - } + let mut rev_path = vec![final_status]; + let mut cursor = final_status; + while cursor != effective_from { + let Some(prev) = cursor.get_previous_status() else { + tracing::debug!( + "Skip graph compensation for {instance_id}:{graph_id}: cannot walk from {final_status:?} back to {effective_from:?}" + ); + return Ok(()); }; + rev_path.push(prev); + cursor = prev; } rev_path.reverse(); for window in rev_path.windows(2) { - let s_from = window[0]; - let s_to = window[1]; - - if let Some(kind) = map_transition_to_event(s_from, s_to) { - match kind { - GraphCompensateEventKind::PreKickoffSent => { - let prekickoff_sent = - PreKickoffSent(crate::action::PreKickoffSent { instance_id, graph_id }); - let message = GOATMessage::new(Actor::All, prekickoff_sent); - push_local_unhandled_messages(local_db, graph_id, &message, 0).await?; - } - GraphCompensateEventKind::KickoffSent => { - let kickoff_sent = - KickoffSent(crate::action::KickoffSent { instance_id, graph_id }); - let message = GOATMessage::new(Actor::All, kickoff_sent); - push_local_unhandled_messages(local_db, graph_id, &message, 0).await?; - } - GraphCompensateEventKind::Take1Sent => { - let take1_sent = Take1Sent(crate::action::Take1Sent { instance_id, graph_id }); - let message = GOATMessage::new(Actor::All, take1_sent); - push_local_unhandled_messages(local_db, graph_id, &message, 0).await?; - } - GraphCompensateEventKind::ChallengeSent => { - let graph = match graph { - Some(g) => g, - None => { - let g = get_graph(local_db, instance_id, graph_id).await?; - match g { - Some(g) => &Bitvm2Graph::from_simplified(&g)?, - None => bail!("Graph {graph_id} not found in local db"), - } - } - }; - let kickoff_txid = graph.kickoff.tx().compute_txid(); - let take1_txid = graph.take1.tx().compute_txid(); - let connector_a_vout = 0; - if let Some(challenge_txid) = - outpoint_spent_txid(btc_client, &kickoff_txid, connector_a_vout).await? - && challenge_txid != take1_txid - { - let challenge_sent = ChallengeSent(crate::action::ChallengeSent { + let [from, to] = window else { + continue; + }; + let Some(event) = map_transition_to_event(*from, *to) else { + continue; + }; + + match event { + GraphCompensateEventKind::PreKickoffSent => { + push_graph_compensate_message( + local_db, + graph_id, + Actor::Verifier, + GOATMessageContent::PreKickoffSent(PreKickoffSent { instance_id, graph_id }), + ) + .await?; + } + GraphCompensateEventKind::KickoffSent => { + push_graph_compensate_message( + local_db, + graph_id, + Actor::All, + GOATMessageContent::KickoffSent(KickoffSent { instance_id, graph_id }), + ) + .await?; + } + GraphCompensateEventKind::Take1Sent => { + push_graph_compensate_message( + local_db, + graph_id, + Actor::Committee, + GOATMessageContent::Take1Sent(Take1Sent { instance_id, graph_id }), + ) + .await?; + } + GraphCompensateEventKind::ChallengeSent => { + if let Some(challenge_txid) = scan.challenge_txid { + push_graph_compensate_message( + local_db, + graph_id, + Actor::Operator, + GOATMessageContent::ChallengeSent(ChallengeSent { instance_id, graph_id, challenge_txid, - }); - let message = GOATMessage::new(Actor::All, challenge_sent); - push_local_unhandled_messages(local_db, graph_id, &message, 0).await?; - } + }), + ) + .await?; } - GraphCompensateEventKind::DisproveSent => { - let sub_status = match final_sub_status { - Some(ref s) => s, - None => { - tracing::error!( - "No final_sub_status provided for DisproveSent compensation!" - ); - continue; - } - }; - let disprove_type = match sub_status.disprove_type { - Some(t) => t, - None => { - tracing::error!( - "No disprove_type in final_sub_status for DisproveSent compensation!" - ); - continue; - } - }; - let graph = match graph { - Some(g) => g, - None => { - let g = get_graph(local_db, instance_id, graph_id).await?; - match g { - Some(g) => &Bitvm2Graph::from_simplified(&g)?, - None => bail!("Graph {graph_id} not found in local db"), - } - } - }; - let kickoff_txid = graph.kickoff.tx().compute_txid(); - let take1_txid = graph.take1.tx().compute_txid(); - let connector_a_vout = 0; - let challenge_start_txid = if let Some(challenge_txid) = - outpoint_spent_txid(btc_client, &kickoff_txid, connector_a_vout).await? - { - if challenge_txid == take1_txid { - tracing::error!("Take1 found for DisproveSent compensation!"); - continue; - } - Some(challenge_txid) - } else { - None - }; - let challenge_finish_txid = match disprove_type { - DisproveTxType::Disprove => { - let connector_e_vout = 3; - outpoint_spent_txid(btc_client, &kickoff_txid, connector_e_vout) - .await? - .ok_or(anyhow!( - "No Disprove txn found for DisproveSent compensation!" - ))? - } - DisproveTxType::OperatorCommitTimeout => { - let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let connector_f_vout = watchtower_num * 2 + 1; - let watchtower_challenge_init_txid = - graph.watchtower_challenge_init.tx().compute_txid(); - outpoint_spent_txid( - btc_client, - &watchtower_challenge_init_txid, - connector_f_vout as u64, - ) - .await? - .ok_or(anyhow!( - "No OperatorCommitTimeout txn found for DisproveSent compensation!" - ))? - } - DisproveTxType::OperatorNack => { - let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let connector_f_vout = watchtower_num * 2 + 1; - let watchtower_challenge_init_txid = - graph.watchtower_challenge_init.tx().compute_txid(); - outpoint_spent_txid( - btc_client, - &watchtower_challenge_init_txid, - connector_f_vout as u64, - ) - .await? - .ok_or(anyhow!( - "No OperatorNack txn found for DisproveSent compensation!" - ))? - } - DisproveTxType::AssertTimeout => { - let assert_commit_num = graph.assert_commit_timeout_txns.len(); - let connector_d_vout = assert_commit_num; - let assert_init_txid = graph.assert_init.tx().compute_txid(); - outpoint_spent_txid( - btc_client, - &assert_init_txid, - connector_d_vout as u64, - ) - .await? - .ok_or(anyhow!( - "No AssertTimeout txn found for DisproveSent compensation!" - ))? - } - DisproveTxType::QuickChallenge => { - let guardian_connector_vout = 4; - outpoint_spent_txid(btc_client, &kickoff_txid, guardian_connector_vout) - .await? - .ok_or(anyhow!( - "No QuickChallenge txn found for DisproveSent compensation!" - ))? - } - DisproveTxType::ChallengeIncompleteKickoff => { - let guardian_connector_vout = 4; - outpoint_spent_txid(btc_client, &kickoff_txid, guardian_connector_vout) - .await? - .ok_or( - anyhow!("No ChallengeIncompleteKickoff txn found for DisproveSent compensation!") - )? - } - }; - let disprove_sent = DisproveSent(crate::action::DisproveSent { + } + GraphCompensateEventKind::DisproveSent => { + let disprove = scan.disprove.clone().ok_or_else(|| { + anyhow!( + "Graph {instance_id}:{graph_id} reached Disprove but no disprove transaction was detected" + ) + })?; + upsert_graph_compensate_message( + local_db, + graph_id, + Some(disprove.index.to_string()), + Actor::Committee, + GOATMessageContent::DisproveSent(DisproveSent { instance_id, graph_id, - disprove_type, - index: sub_status.disprove_index as usize, - challenge_start_txid, - challenge_finish_txid, - }); - let message = GOATMessage::new(Actor::All, disprove_sent); - push_local_unhandled_messages(local_db, graph_id, &message, 0).await?; - } - GraphCompensateEventKind::Take2Sent => { - let take2_sent = Take2Sent(crate::action::Take2Sent { instance_id, graph_id }); - let message = GOATMessage::new(Actor::All, take2_sent); - push_local_unhandled_messages(local_db, graph_id, &message, 0).await?; - } + disprove_type: disprove.disprove_type, + index: disprove.index, + challenge_start_txid: disprove.challenge_start_txid, + challenge_finish_txid: disprove.challenge_finish_txid, + }), + ) + .await?; + } + GraphCompensateEventKind::Take2Sent => { + push_graph_compensate_message( + local_db, + graph_id, + Actor::Committee, + GOATMessageContent::Take2Sent(Take2Sent { instance_id, graph_id }), + ) + .await?; } } } @@ -1361,7 +1483,7 @@ pub(crate) async fn compensate_graph_events( Ok(()) } -pub fn build_graph_data(graph: &Bitvm2Graph) -> Result { +pub fn build_graph_data(graph: &BitvmGcGraph) -> Result { // operator pubkey: first byte is prefix, next 32 bytes are key let op_pk_bytes = graph.parameters.operator_pubkey.to_bytes(); let operator_pubkey_prefix = op_pk_bytes[0]; @@ -1373,15 +1495,23 @@ pub fn build_graph_data(graph: &Bitvm2Graph) -> Result { let kickoff_txid = graph.kickoff.finalize().compute_txid().to_byte_array(); let take1_txid = graph.take1.finalize().compute_txid().to_byte_array(); let take2_txid = graph.take2.finalize().compute_txid().to_byte_array(); - let commit_timout_txid = - graph.blockhash_commit_timeout.finalize().compute_txid().to_byte_array(); - let assert_timeout_txids: Vec<[u8; 32]> = graph - .assert_commit_timeout_txns + let watchtower_challenge_init_txid = + graph.watchtower_challenge_init.finalize().compute_txid().to_byte_array(); + let prover_assert_txid = graph.operator_assert.finalize().compute_txid().to_byte_array(); + let disprove_txids: Vec<[u8; 32]> = + graph.disproves.iter().map(|tx| tx.finalize().compute_txid().to_byte_array()).collect(); + let watchtower_challenge_timeout_txids: Vec<[u8; 32]> = graph + .watchtower_challenge_timeouts .iter() .map(|tx| tx.finalize().compute_txid().to_byte_array()) .collect(); - let nack_txids: Vec<[u8; 32]> = - graph.nack_txns.iter().map(|tx| tx.finalize().compute_txid().to_byte_array()).collect(); + let operator_challenge_nack_txids: Vec<[u8; 32]> = graph + .operator_challenge_nacks + .iter() + .map(|tx| tx.finalize().compute_txid().to_byte_array()) + .collect(); + let operator_commit_timeout_txid = + graph.operator_commit_timeout.finalize().compute_txid().to_byte_array(); Ok(GraphData { operator_pubkey_prefix, @@ -1390,13 +1520,16 @@ pub fn build_graph_data(graph: &Bitvm2Graph) -> Result { kickoff_txid, take1_txid, take2_txid, - commit_timout_txid, - assert_timeout_txids, - nack_txids, + watchtower_challenge_init_txid, + prover_assert_txid, + disprove_txids, + watchtower_challenge_timeout_txids, + operator_challenge_nack_txids, + operator_commit_timeout_txid, }) } -pub async fn get_graph_digest(goat_client: &GOATClient, graph: &Bitvm2Graph) -> Result<[u8; 32]> { +pub async fn get_graph_digest(goat_client: &GOATClient, graph: &BitvmGcGraph) -> Result<[u8; 32]> { let instance_id = graph.parameters.instance_parameters.instance_id; let graph_id = graph.parameters.graph_id; let graph_data = build_graph_data(graph)?; @@ -1535,7 +1668,7 @@ pub async fn read_pegin_request( pub async fn read_instance_info_from_goat( goat_client: &GOATClient, instance_id: Uuid, -) -> Result { +) -> Result { let pegin_data = goat_client.gateway_get_pegin_data(&instance_id).await?; let network = get_network(); let user_change_address = Address::from_str(&pegin_data.user_change_addr) @@ -1586,7 +1719,7 @@ pub async fn read_instance_info_from_goat( } }; let committee_agg_pubkey = generate_n_of_n_public_key(&committee_pubkeys).0; - Ok(Bitvm2InstanceParameters { + Ok(BitvmGcInstanceParameters { network, instance_id, user_info, @@ -1604,46 +1737,11 @@ pub async fn is_take1_timelock_expired(client: &BTCClient, kickoff_height: u32) pub async fn is_take2_timelock_expired( client: &BTCClient, - watchtower_challenge_init_height: u32, - assert_init_height: u32, + operator_assert_height: u32, ) -> Result { - let lock_blocks = take2_timelocks(get_network()); + let lock_blocks = take2_timelock(get_network()); let current_height = client.get_height().await?; - Ok(current_height >= watchtower_challenge_init_height + lock_blocks.0 - || current_height >= assert_init_height + lock_blocks.1) -} - -/// Loads partial scripts from a local cache file. -/// If cache file does not exist, generate partial scripts by vk an cache it -pub async fn get_partial_scripts() -> Result> { - let scripts_cache_path = format!("{SCRIPT_CACHE_FILE_NAME}.bin"); - if Path::new(&scripts_cache_path).exists() { - let file = File::open(scripts_cache_path)?; - let reader = BufReader::new(file); - let scripts_bytes: Vec = bincode::deserialize_from(reader)?; - Ok(scripts_bytes) - } else { - let partial_scripts = generate_partial_scripts(&get_vk().await?); - if let Some(parent) = Path::new(&scripts_cache_path).parent() { - fs::create_dir_all(parent)?; - }; - let file = File::create(&scripts_cache_path)?; - let writer = BufWriter::new(file); - bincode::serialize_into(writer, &partial_scripts)?; - Ok(partial_scripts) - } -} - -pub async fn get_disprove_scripts(graph_params: &Bitvm2GraphParameters) -> Result> { - let partial_scripts = get_partial_scripts().await?; - let (mut disprove_scripts, disprove_scripts_1) = generate_disprove_scripts( - &partial_scripts, - graph_params.operator_wots_pubkeys.clone(), - &graph_params.guest_constant_value, - &graph_params.hashlocks, - ); - disprove_scripts.extend(disprove_scripts_1); - Ok(disprove_scripts) + Ok(current_height >= operator_assert_height + lock_blocks) } pub async fn get_fee_rate(client: &BTCClient) -> Result { @@ -1752,27 +1850,27 @@ pub async fn broadcast_package( fn gen_watchtower_commitment(graph_id: Uuid, proof_data: ProofData) -> Result> { let graph_id = graph_id.as_bytes(); - let proof = proof_data.proof.as_slice(); + let proof = + proof_data.proof.as_slice().try_into().map_err(|_| anyhow!("invalid proof length"))?; + let public_inputs = proof_data + .public_inputs + .as_slice() + .try_into() + .map_err(|_| anyhow!("invalid public inputs length"))?; if proof_data.vk.len() != VK_HASH_SIZE { bail!("invalid vk_hash length"); } - if proof_data.proof_part_stark_vk.is_empty() { - bail!("missing proof_part_stark_vk"); + if proof_data.zkm_version.is_empty() { + bail!("missing zkm_version"); } - build_watchtower_commitment( + Ok(build_watchtower_commitment( graph_id, proof, - &proof_data.public_inputs, + &public_inputs, &proof_data.vk, - &proof_data.proof_part_stark_vk, - ) - .map_err(|e| anyhow!("failed to build watchtower commitment: {e}")) -} - -fn load_part_stark_vk_for_zkm_version(zkm_version: &str) -> Result> { - catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec())) - .map_err(|_| anyhow!("failed to load part_stark_vk for zkm_version {zkm_version}")) + &proof_data.zkm_version, + )) } // proof network @@ -1832,261 +1930,233 @@ pub async fn get_watchtower_commitment( pub async fn get_watchtower_challenge_info( btc_client: &BTCClient, watchtower_challenge_init_txid: &SerializableTxid, - num_challenger: usize, -) -> Result<(Vec, Vec)> { - let challenge_finish_txids: Vec> = try_join_all((0..num_challenger).map(|i| { - let connector_vout = i as u32 * 2; - outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid.0, connector_vout.into()) - })) - .await?; - - if challenge_finish_txids.iter().any(|txid| txid.is_none()) { - bail!( - "No enough watchtower challenge tx found for graph, expected {num_challenger} but got {}. Waiting for watchtower challenge to be submitted", - challenge_finish_txids.iter().filter(|txid| txid.is_some()).count() - ); - } - - let challenge_timeout_txids: Vec> = try_join_all((0..num_challenger).map(|i| { - let connector_vout = i as u32 * 2 + 1; - outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid.0, connector_vout.into()) - })) - .await?; - - // calculate challenge txid and included watchtower map - let mut included_watchtowers: Vec = vec![false; num_challenger]; - let watchtower_challenge_txids: Vec = - (included_watchtowers.iter_mut().zip(challenge_finish_txids.iter())) - .zip(challenge_timeout_txids.iter()) - .map(|((included, finish_txid), timeout_txid)| { - if finish_txid.is_some() && timeout_txid.is_some() && finish_txid != timeout_txid { - *included = true; - } - finish_txid.unwrap().to_string() - }) - .collect(); - - if watchtower_challenge_txids.len() != num_challenger { - bail!( - "challenge txids length mismatch with num_challenger, expected {num_challenger} but got {}", - watchtower_challenge_txids.len() - ); - } - Ok((watchtower_challenge_txids, included_watchtowers)) -} -/// Returns: -/// - `Ok(Some(OperatorProof), _)` if operator proof is available -/// - `Ok(None, wait_secs)` if operator proof is not yet available, with suggested wait time -pub async fn get_operator_proof( - local_db: &LocalDB, - http_client: &HttpAsyncClient, - bitvm_graph: &Bitvm2Graph, - btc_client: &BTCClient, - instance_id: Uuid, - graph_id: Uuid, - operator_committed_blockhash: String, -) -> Result<(Option<(GuestInputs, Groth16Proof, PublicInputs, VerifyingKey)>, usize)> { - let mut storage_processor = local_db.acquire().await?; - if let Some(graph) = storage_processor.find_graph(&graph_id).await? { - if graph.proceed_withdraw_height <= 0 { - warn!("graph {graph_id} proceed_withdraw_height <= 0, waiting to been updated"); - return Ok((None, get_operator_proof_wait_secs())); - } - - let watchtower_challenge_init_txid = graph - .watchtower_challenge_init_txid - .ok_or_else(|| anyhow::anyhow!("watchtower_challenge_init_txid is none"))?; - let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); - let (watchtower_challenge_txids, included_watchtowers) = - match get_watchtower_challenge_info( - btc_client, - &watchtower_challenge_init_txid, - num_challenger, - ) - .await - { - Ok(info) => info, - Err(e) => { - warn!("Failed to get watchtower challenge info: {e}"); - return Ok((None, get_operator_proof_wait_secs())); + num_watchtowers: usize, +) -> Result<(Vec>, Vec)> { + let mut challenge_txids = Vec::with_capacity(num_watchtowers); + let mut included_watchtowers = Vec::with_capacity(num_watchtowers); + for index in 0..num_watchtowers { + let challenge_vout = + output_topology::watchtower_challenge_init::watchtower_connector(index) as u64; + let spent_txid = + outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid.0, challenge_vout) + .await?; + match spent_txid { + Some(txid) => { + let status = btc_client.get_tx_status(&txid).await?; + if !status.confirmed { + bail!("watchtower challenge tx {txid} at index {index} is not confirmed yet"); } - }; - let base_url = Url::parse( - &get_proof_build_rpc_host() - .ok_or_else(|| anyhow::anyhow!("failed to get proof_build_rpc_host"))?, - )?; - let url = base_url.join(NODES_OPERATOR_BASE)?; - - let response = http_client - .post_response_json::( - url.as_str(), - &OperatorProofRequest { - instance_id: instance_id.to_string(), - graph_id: graph_id.to_string(), - operator_committed_blockhash, - execution_layer_block_number: graph.proceed_withdraw_height, - watchtower_challenge_txids, - included_watchtowers, - watchtower_challenge_init_txid: watchtower_challenge_init_txid.0.to_string(), - watchtower_challenge_pubkeys: bitvm_graph - .parameters - .watchtower_pubkeys - .iter() - .map(|pk| pk.public_key(secp256k1::Parity::Even).to_string()) - .collect(), - }, - ) - .await?; - - match response.proof_data { - Some(proof_data) => { - info!("get_operator_proof get proof successfully"); - let proof: ZKMProofWithPublicValues = - bincode::deserialize(proof_data.proof.as_slice()).unwrap(); - let proof_part_stark_vk = load_part_stark_vk_for_zkm_version(&proof.zkm_version)?; - let output: bitcoin_light_client_circuit::OperatorPublicOutputs = - proof.public_values.clone().read(); - // TODO: additionally check constant and included_watchtower with included_watchtowers. - //proof.public_values.head(); - info!("get_operator_proof parse proof successfully"); - let ark_proof = convert_ark_imm_wrap_vk( - &proof, - &proof_data.vk, - &IMM_GROTH16_VK_BYTES, - &proof_part_stark_vk, - ) - .map_err(|e| anyhow!("failed to convert operator proof to ark format: {e}"))?; - info!("get_operator_proof parse proof successfully"); - - Ok(( - Some(( - [output.constant, output.included_watchtowers], - ark_proof.proof.clone(), - ark_proof.public_inputs.into(), - ark_proof.groth16_vk.into(), - )), - 0, - )) + challenge_txids.push(Some(txid.to_string())); + included_watchtowers.push(true); + } + None => { + challenge_txids.push(None); + included_watchtowers.push(false); } - None => Ok((None, get_operator_proof_wait_secs())), } - } else { - warn!("graph:{graph_id} not found"); - bail!("No graph in db"); } + Ok((challenge_txids, included_watchtowers)) } -const ASSERT_COMMIT_CACHE_VERSION: u32 = 1; - -#[derive(Serialize, Deserialize)] -struct CachedAssertCommitInput { - txin: Vec, - amount_sat: u64, -} - -#[derive(Serialize, Deserialize)] -struct CachedAssertCommitInputs { - version: u32, - inputs: Vec, +fn load_part_stark_vk_for_zkm_version(zkm_version: &str) -> Result> { + catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec())) + .map_err(|_| anyhow!("failed to load part_stark_vk for zkm_version {zkm_version}")) } -fn embed_txin_into_dummy_tx(txin: &TxIn) -> Transaction { - Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![txin.clone()], - output: vec![], +fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { + if !wrapper_vk_hash.starts_with("0x") { + bail!("configured wrapper vk hash must use 0x-prefixed Ziren encoding"); + } + let raw_vk_hash = decode_zkm_vkey_hash(wrapper_vk_hash) + .map_err(|e| anyhow!("invalid configured wrapper vk hash: {e:?}"))?; + let part_vk: PartStarkVerifyingKey = + bincode::deserialize(&load_part_stark_vk_for_zkm_version(zkm_version)?) + .context("deserialize configured wrapper partial STARK verifying key")?; + let base = Bn254Fr::from_canonical_u32(256); + let mut field_hash = Bn254Fr::ZERO; + for byte in raw_vk_hash { + field_hash = field_hash * base + Bn254Fr::from_canonical_u32(byte as u32); } + let combined = zkm_recursion_core::hash_vkey_with_part_vk(&part_vk, field_hash); + let bytes = combined.as_canonical_biguint().to_bytes_be(); + if bytes.len() > 32 { + bail!("combined wrapper verifying key hash exceeds BN254 field encoding"); + } + let mut encoded = [0u8; 32]; + encoded[32 - bytes.len()..].copy_from_slice(&bytes); + Ok(encoded) } -fn extract_txin_from_dummy_tx(tx: &Transaction) -> TxIn { - tx.input[0].clone() +pub fn derive_operator_wrapper_statement(graph_id: Uuid) -> Result { + let public_values = wrapper_public_values( + get_operator_vk_hash()?, + *graph_id.as_bytes(), + get_genesis_sequencer_commit_id(), + ) + .to_vec(); + let wrapper_vk_hash = get_operator_wrapper_vk_hash()?; + let zkm_version = get_operator_wrapper_zkm_version()?; + let public_inputs = load_ark_public_inputs_from_bytes( + &combined_wrapper_vk_hash(&wrapper_vk_hash, &zkm_version)?, + &hash_public_inputs(&public_values), + ) + .to_vec(); + Ok(OperatorWrapperStatement { public_values, public_inputs, wrapper_vk_hash, zkm_version }) } -fn assert_commit_cache_path(graph_id: Uuid) -> PathBuf { - Path::new(ASSERT_COMMITS_CACHE_DIR).join(format!("{graph_id}.json")) -} +/// Returns: +/// - `Ok(Some(WrapperProof), _)` if wrapper proof is available +/// - `Ok(None, wait_secs)` if operator or wrapper proof is not yet available +pub async fn get_operator_wrapper_proof( + local_db: &LocalDB, + http_client: &HttpAsyncClient, + bitvm_graph: &BitvmGcGraph, + btc_client: &BTCClient, + instance_id: Uuid, + graph_id: Uuid, +) -> Result<(Option, usize)> { + let mut storage_processor = local_db.acquire().await?; + let Some(graph) = storage_processor.find_graph(&graph_id).await? else { + warn!("graph:{graph_id} not found"); + bail!("No graph in db"); + }; + drop(storage_processor); -fn load_assert_commit_inputs_from_cache(graph_id: Uuid) -> Option> { - let path = assert_commit_cache_path(graph_id); - if !path.exists() { - return None; + if graph.proceed_withdraw_height <= 0 { + warn!("graph {graph_id} proceed_withdraw_height <= 0, waiting to been updated"); + return Ok((None, get_operator_proof_wait_secs())); } - let file = match File::open(&path) { - Ok(file) => file, - Err(err) => { - warn!("failed to open assert-commit cache {path:?}: {err:?}"); - return None; - } - }; - let reader = BufReader::new(file); - let cached: CachedAssertCommitInputs = match serde_json::from_reader(reader) { - Ok(data) => data, - Err(err) => { - warn!("failed to deserialize assert-commit cache {path:?}: {err:?}"); - return None; + + let watchtower_challenge_init_txid = graph + .watchtower_challenge_init_txid + .ok_or_else(|| anyhow::anyhow!("watchtower_challenge_init_txid is none"))?; + let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); + let (watchtower_challenge_txids, included_watchtowers) = match get_watchtower_challenge_info( + btc_client, + &watchtower_challenge_init_txid, + num_challenger, + ) + .await + { + Ok(info) => info, + Err(e) => { + warn!("Failed to get watchtower challenge info: {e}"); + return Ok((None, get_operator_proof_wait_secs())); } }; - if cached.version != ASSERT_COMMIT_CACHE_VERSION { - warn!( - "assert-commit cache version mismatch for {path:?}, expecting {ASSERT_COMMIT_CACHE_VERSION}, got {}", - cached.version - ); - return None; - } - let mut inputs = Vec::with_capacity(cached.inputs.len()); - for item in cached.inputs { - match deserialize::(&item.txin) { - Ok(txin_embed_tx) => inputs.push(( - extract_txin_from_dummy_tx(&txin_embed_tx), - Amount::from_sat(item.amount_sat), - )), - Err(err) => { - warn!("failed to decode txin from cache {path:?}: {err:?}"); - return None; + let operator_committed_blockhash = { + let mut largest: Option<(u32, BlockHash)> = None; + for txid in watchtower_challenge_txids.iter().flatten() { + let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; + let (height, hash) = match (status.block_height, status.block_hash) { + (Some(height), Some(hash)) => (height, hash), + _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), + }; + if largest.is_none_or(|(largest_height, _)| height > largest_height) { + largest = Some((height, hash)); } } + largest + .map(|(_, hash)| hash.to_string()) + .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx is available"))? + }; + + let base_url = Url::parse( + &get_proof_build_rpc_host() + .ok_or_else(|| anyhow::anyhow!("failed to get proof_build_rpc_host"))?, + )?; + let operator_url = base_url.join(NODES_OPERATOR_BASE)?; + let operator_response = http_client + .post_response_json::( + operator_url.as_str(), + &OperatorProofRequest { + instance_id: instance_id.to_string(), + graph_id: graph_id.to_string(), + operator_committed_blockhash, + execution_layer_block_number: graph.proceed_withdraw_height, + watchtower_challenge_txids, + included_watchtowers, + watchtower_challenge_init_txid: watchtower_challenge_init_txid.0.to_string(), + watchtower_challenge_pubkeys: bitvm_graph + .parameters + .watchtower_pubkeys + .iter() + .map(|pk| pk.public_key(secp256k1::Parity::Even).to_string()) + .collect(), + }, + ) + .await?; + + if operator_response.proof_data.is_none() { + return Ok((None, get_operator_proof_wait_secs())); } - Some(inputs) -} -fn store_assert_commit_inputs_in_cache(graph_id: Uuid, inputs: &[(TxIn, Amount)]) -> Result<()> { - fs::create_dir_all(ASSERT_COMMITS_CACHE_DIR)?; - let path = assert_commit_cache_path(graph_id); - let file = File::create(&path)?; - let writer = BufWriter::new(file); - let payload = CachedAssertCommitInputs { - version: ASSERT_COMMIT_CACHE_VERSION, - inputs: inputs - .iter() - .map(|(txin, amount)| CachedAssertCommitInput { - txin: serialize(&embed_txin_into_dummy_tx(txin)), - amount_sat: amount.to_sat(), - }) - .collect(), + let statement = derive_operator_wrapper_statement(graph_id)?; + let expected_public_values = statement.public_values.clone(); + let genesis_txid_text = std::env::var(ENV_GENESIS_SEQUENCER_COMMIT_TXID) + .map_err(|_| anyhow!("{ENV_GENESIS_SEQUENCER_COMMIT_TXID} needs to be set"))?; + + let mut wrapper_url = base_url.join(PROOFS_WRAPPER_PROOF)?; + wrapper_url + .query_pairs_mut() + .append_pair("instance_id", &instance_id.to_string()) + .append_pair("graph_id", &graph_id.to_string()) + .append_pair("genesis_sequencer_commit_txid", &genesis_txid_text); + let wrapper_response = + http_client.get_response_json::(wrapper_url.as_str()).await?; + + let Some(proof_data) = wrapper_response.proof_data else { + if let Some(error) = wrapper_response.error { + info!("operator wrapper proof is not ready for graph_id:{graph_id}: {error}"); + } + return Ok((None, get_operator_proof_wait_secs())); }; - serde_json::to_writer(writer, &payload)?; - Ok(()) -} -fn cleanup_assert_commit_cache(graph_id: Uuid) -> Result<()> { - let path = assert_commit_cache_path(graph_id); - if path.exists() { - fs::remove_file(path)?; + let proof: ZKMProofWithPublicValues = bincode::deserialize(proof_data.proof.as_slice()) + .map_err(|err| anyhow!("failed to deserialize operator wrapper proof: {err}"))?; + let proof_public_values = proof.public_values.to_vec(); + if proof_public_values != expected_public_values { + bail!("operator wrapper proof public values do not match graph challenge inputs"); + } + if !proof_data.public_inputs.is_empty() && proof_data.public_inputs != expected_public_values { + bail!("operator wrapper proof public input sidecar does not match proof"); + } + let expected_wrapper_vk_hash = statement.wrapper_vk_hash.clone(); + if proof_data.vk != expected_wrapper_vk_hash { + bail!("operator wrapper proof vk hash does not match configured wrapper identity"); + } + let expected_zkm_version = statement.zkm_version.clone(); + if proof.zkm_version != expected_zkm_version || proof_data.zkm_version != expected_zkm_version { + bail!("operator wrapper proof Ziren version does not match configured wrapper identity"); } - Ok(()) -} -pub async fn challenger_force_skip_kickoff( - client: &BTCClient, - graph: &Bitvm2Graph, -) -> Result { - let challenger_master_key = ChallengerMasterKey::new(get_bitvm_key()?); - let challenger_master_keypair = challenger_master_key.master_keypair(); - let challenger_receive_address = - node_p2wsh_address(get_network(), &challenger_master_keypair.public_key().into()); + let part_stark_vk = load_part_stark_vk_for_zkm_version(&proof.zkm_version)?; + let ark_proof = + convert_ark_imm_wrap_vk(&proof, &proof_data.vk, &IMM_GROTH16_VK_BYTES, &part_stark_vk) + .map_err(|e| anyhow!("failed to convert operator wrapper proof to ark format: {e}"))?; + if ark_proof.public_inputs.as_slice() != statement.public_inputs.as_slice() { + bail!("operator wrapper proof public inputs do not match graph setup statement"); + } + + Ok(( + Some(ValidatedOperatorWrapperProof { + proof: ark_proof.proof.clone(), + public_inputs: ark_proof.public_inputs.into(), + verifying_key: ark_proof.groth16_vk.into(), + public_values: expected_public_values, + wrapper_vk_hash: expected_wrapper_vk_hash, + zkm_version: expected_zkm_version, + }), + 0, + )) +} +pub async fn verifier_force_skip_kickoff(client: &BTCClient, graph: &BitvmGcGraph) -> Result { + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let verifier_master_keypair = verifier_master_key.master_keypair(); + let verifier_receive_address = + node_p2wsh_address(get_network(), &verifier_master_keypair.public_key().into()); let fee_rate = get_fee_rate(client).await?; let (force_skip_kickoff_tx, anchor_added) = - build_force_skip_kickoff_tx(graph, challenger_receive_address, fee_rate)?; + build_force_skip_kickoff_tx(graph, verifier_receive_address, fee_rate)?; if anchor_added { let anchor_vout = force_skip_kickoff_tx.output.len() as u64 - 1; let force_skip_kickoff_tx_total_input_amount = @@ -2110,14 +2180,14 @@ pub async fn challenger_force_skip_kickoff( Ok(force_skip_kickoff_tx.compute_txid()) } -pub async fn challenger_quick_challenge(client: &BTCClient, graph: &Bitvm2Graph) -> Result { - let challenger_master_key = ChallengerMasterKey::new(get_bitvm_key()?); - let challenger_master_keypair = challenger_master_key.master_keypair(); - let challenger_receive_address = - node_p2wsh_address(get_network(), &challenger_master_keypair.public_key().into()); +pub async fn verifier_quick_challenge(client: &BTCClient, graph: &BitvmGcGraph) -> Result { + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let verifier_master_keypair = verifier_master_key.master_keypair(); + let verifier_receive_address = + node_p2wsh_address(get_network(), &verifier_master_keypair.public_key().into()); let fee_rate = get_fee_rate(client).await?; let (quick_challenge_tx, anchor_added) = - build_quick_challenge_tx(graph, challenger_receive_address, fee_rate)?; + build_quick_challenge_tx(graph, verifier_receive_address, fee_rate)?; if anchor_added { let anchor_vout = quick_challenge_tx.output.len() as u64 - 1; let quick_challenge_tx_total_input_amount = @@ -2380,6 +2450,36 @@ pub async fn build_cpfp_txns( } } +pub async fn broadcast_tx_with_cpfp( + btc_client: &BTCClient, + parent_tx: Transaction, + parent_tx_total_input_amount: Amount, +) -> Result<()> { + let anchor_output = p2a_output(); + let anchor_vout = parent_tx + .output + .iter() + .position(|output| output == &anchor_output) + .ok_or_else(|| anyhow!("cannot CPFP transaction without a P2A anchor output"))?; + if let Some(duplicate_vout) = parent_tx + .output + .iter() + .enumerate() + .skip(anchor_vout + 1) + .find_map(|(vout, output)| if output == &anchor_output { Some(vout) } else { None }) + { + bail!("transaction has multiple P2A anchor outputs at {anchor_vout} and {duplicate_vout}"); + }; + let child_tx = + build_cpfp_txns(btc_client, &parent_tx, anchor_vout as u64, parent_tx_total_input_amount) + .await?; + match child_tx { + Some(tx) => broadcast_package(btc_client, &[parent_tx, tx], true).await?, + None => broadcast_tx(btc_client, &parent_tx).await?, + }; + Ok(()) +} + /// Returns: /// - `Ok(None)` if given address does not have enough btc, /// - `Ok(Some((utxos, fee_amount, change_amount)))` @@ -2573,8 +2673,8 @@ pub async fn build_genesis_prekickoff_tx( btc_client: &BTCClient, goat_client: &GOATClient, ) -> Result { - let assert_commit_num = todo_funcs::assert_commmit_num(); let watchtower_num = goat_client.committee_mana_get_watchtowers().await?.len(); + let verifier_num = todo_funcs::min_required_verifier(); let network = get_network(); let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let node_keypair = operator_master_key.master_keypair(); @@ -2605,7 +2705,7 @@ pub async fn build_genesis_prekickoff_tx( vec![], fee_amount.to_sat(), watchtower_num, - assert_commit_num, + verifier_num, ) .map_err(|e| anyhow::anyhow!("failed to create pre-kickoff txn: {e}")) } @@ -2615,7 +2715,10 @@ pub async fn build_prekickoff_params( graph_nonce: u64, cur_prekickoff_txn: PrekickoffTransaction, ) -> Result { - let prekickoff_remaining_amount = cur_prekickoff_txn.tx().output[2].value; + let prekickoff_remaining_amount = cur_prekickoff_txn + .prekickoff_connector_input() + .map_err(|e| anyhow!("failed to get pre-kickoff connector input: {e}"))? + .amount; let (replenish_fee_inputs, replenish_fee_prev_outs, fee_amount) = if prekickoff_remaining_amount >= todo_funcs::min_prekickoff_input_amount() { @@ -2665,44 +2768,52 @@ pub async fn build_prekickoff_params( } pub async fn build_graph_params( - local_db: &LocalDB, + _local_db: &LocalDB, goat_client: &GOATClient, - instance_parameters: Bitvm2InstanceParameters, + instance_parameters: BitvmGcInstanceParameters, prekickoff_parameters: PrekickoffParameters, + bitvm_gc_circuit_datas: Vec, graph_nonce: u64, graph_id: Uuid, -) -> Result { +) -> Result { let instance_id = instance_parameters.instance_id; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let operator_master_keypair = operator_master_key.master_keypair(); let operator_pubkey = operator_master_keypair.public_key().into(); let operator_receive_address = node_p2wsh_address(instance_parameters.network, &operator_pubkey); - let operator_wots_pubkeys = operator_master_key.wots_keypair_for_graph(graph_id).1; + let (_, operator_assert_wots_pubkey) = + operator_master_key.assert_wots_keypair_for_graph(graph_id); + let (_, operator_commit_pubin_wots_pubkey) = + operator_master_key.commit_pubin_wots_keypair_for_graph(graph_id); let watchtower_pubkeys = goat_client.committee_mana_get_watchtowers().await?; - let mut hashlocks = vec![]; - for index in 0..watchtower_pubkeys.len() { - let preimage = todo_funcs::get_preimage(local_db, instance_id, graph_id, index).await?; - let hashlock = hash160(&preimage); - hashlocks.push(hashlock); - } - let guest_constant_value = get_guest_constant_value(instance_id, graph_id)?; - Ok(Bitvm2GraphParameters { + let watchtower_ack_hashlocks = (0..watchtower_pubkeys.len()) + .map(|index| { + bitcoin::hashes::hash160::Hash::hash( + &operator_master_key.preimage_for_graph(graph_id, index), + ) + .to_byte_array() + }) + .collect(); + let pubin_disprove_constant = get_guest_constant_value(instance_id, graph_id)?; + Ok(BitvmGcGraphParameters { instance_parameters, prekickoff_parameters, graph_id, graph_nonce, challenge_amount: todo_funcs::challenge_amount(), operator_pubkey, - operator_wots_pubkeys, + operator_assert_wots_pubkey, + operator_commit_pubin_wots_pubkey, operator_receive_address, watchtower_pubkeys, - hashlocks, - guest_constant_value, + watchtower_ack_hashlocks, + pubin_disprove_constant, + gc_data: bitvm_gc_circuit_datas, }) } -pub async fn operator_skip_graph(btc_client: &BTCClient, graph: &mut Bitvm2Graph) -> Result<()> { +pub async fn operator_skip_graph(btc_client: &BTCClient, graph: &mut BitvmGcGraph) -> Result<()> { let graph_nonce = graph.parameters.graph_nonce; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let operator_master_keypair = operator_master_key.master_keypair(); @@ -2767,7 +2878,7 @@ pub async fn operator_skip_graph(btc_client: &BTCClient, graph: &mut Bitvm2Graph Ok(()) } -pub async fn operator_kickoff(btc_client: &BTCClient, graph: &mut Bitvm2Graph) -> Result<()> { +pub async fn operator_kickoff(btc_client: &BTCClient, graph: &mut BitvmGcGraph) -> Result<()> { let graph_nonce = graph.parameters.graph_nonce; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let operator_graph_keypair = operator_master_key.master_keypair(); @@ -2837,221 +2948,24 @@ pub async fn operator_kickoff(btc_client: &BTCClient, graph: &mut Bitvm2Graph) - Ok(()) } -/// Return values: (Some(split_txid), has_pending_fee_inputs, Some(wait_proof_gen_secs)) -/// - `split_txid`: If a split transaction was broadcasted to consolidate UTXOs for fees, its txid is returned here. -/// - `has_pending_fee_inputs`: Indicates whether some of UTXOs for fees are still pending. -/// - `wait_proof_gen_secs`: If proof generation is still in progress, this returns the estimated time in seconds to wait before retrying. -pub async fn operator_send_assert_commit( - local_db: &LocalDB, - btc_client: &BTCClient, - http_client: &HttpAsyncClient, - graph: &mut Bitvm2Graph, -) -> Result<(Option, bool, Option)> { - // Prepare keys and proof materials - let instance_id = graph.parameters.instance_parameters.instance_id; - let graph_id = graph.parameters.graph_id; - let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); - let node_keypair = operator_master_key.master_keypair(); - let node_public_key: PublicKey = node_keypair.public_key().into(); - let node_address = node_p2wsh_address(get_network(), &node_public_key); - let fee_rate = get_fee_rate(btc_client).await?; - - // Ensure assert-init is confirmed before sending commits - let assert_init_txid = graph.assert_init.tx().compute_txid(); - if !tx_confirmed(btc_client, &assert_init_txid).await? { - bail!("assert-init not confirmed yet, skip assert-commit broadcast"); - } - - // Build or load signed inputs for each assert-commit connector - let assert_commit_inputs = if let Some(inputs) = load_assert_commit_inputs_from_cache(graph_id) - { - tracing::info!("loaded assert-commit inputs from cache for graph_id:{graph_id}"); - inputs - } else { - let wots_secret_keys = operator_master_key.wots_keypair_for_graph(graph_id).0; - let operator_committed_blockhash = - get_largest_watchtower_challenge_block(graph, btc_client).await?; - let (guest_inputs, proof, groth16_pubin, vk) = match get_operator_proof( - local_db, - http_client, - graph, - btc_client, - instance_id, - graph_id, - operator_committed_blockhash.to_string(), - ) - .await? - { - (Some(proof_data), _) => proof_data, - (None, wait_secs) => { - tracing::info!( - "operator proof generation in progress for graph_id:{graph_id}, wait and retry {wait_secs}s later" - ); - return Ok((None, false, Some(wait_secs))); - } - }; - info!("operator_send_assert_commit start operator_sign_assert_commit"); - let inputs = operator_sign_assert_commit( - node_keypair, - graph, - &wots_secret_keys, - guest_inputs, - proof, - groth16_pubin, - &vk, - )?; - - info!("operator_send_assert_commit start store_assert_commit_inputs_in_cache"); - if let Err(err) = store_assert_commit_inputs_in_cache(graph_id, &inputs) { - tracing::warn!("failed to write assert-commit cache for graph_id:{graph_id}: {err:?}"); - } - inputs - }; - - fn estimate_fee_funding_amount(txin: &TxIn, fee_rate: f64) -> Amount { - let sample_tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![txin.clone()], - output: vec![TxOut { - value: Amount::ZERO, - script_pubkey: generate_opreturn_script(vec![]), - }], - }; - let base_vbytes = sample_tx.weight().to_vbytes_ceil(); - let est_vbytes = base_vbytes + CHEKSIG_P2WSH_INPUT_VBYTES + P2WSH_OUTPUT_VBYTES; - let est_fee = (est_vbytes as f64 * fee_rate).ceil() as u64; - Amount::from_sat(est_fee + DUST_AMOUNT + 1_000) - } - - // filter out already-spent assert-commit connectors - let mut pending_assert_commit_txins: Vec<(usize, TxIn, Amount)> = vec![]; - let mut required_fees: Vec = vec![]; - for (i, (txin, amount)) in assert_commit_inputs.into_iter().enumerate() { - if outpoint_spent_txid(btc_client, &assert_init_txid, i as u64).await?.is_none() { - let est_fee = estimate_fee_funding_amount(&txin, fee_rate); - pending_assert_commit_txins.push((i, txin, amount)); - required_fees.push(est_fee); - } - } - if pending_assert_commit_txins.is_empty() { - tracing::info!("no assert-commit inputs to send (all spent)"); - if let Err(err) = cleanup_assert_commit_cache(graph_id) { - tracing::warn!( - "failed to cleanup assert-commit cache for graph_id:{graph_id}: {err:?}" - ); - } - return Ok((None, false, None)); - } - - // get available fee UTXOs from node address - let (utxo_sets, split_tx) = - get_proper_utxo_sets(btc_client, node_address.clone(), required_fees.clone(), fee_rate) - .await?; - - // broadcast split tx if needed - if let Some((mut split_tx, txin_amounts)) = split_tx { - for (i, amount) in txin_amounts.iter().enumerate().take(split_tx.input.len()) { - node_sign(&mut split_tx, i, *amount, EcdsaSighashType::All, &node_keypair)?; - } - let split_txid = split_tx.compute_txid(); - broadcast_tx(btc_client, &split_tx).await?; - return Ok((Some(split_txid), false, None)); - } else if utxo_sets.is_empty() { - let current_balance = btc_client - .get_address_utxo(node_address) - .await? - .iter() - .map(|u| u.value) - .sum::(); - let required_total_fee: Amount = required_fees.into_iter().sum(); - bail!(SpecialError::InsufficientBalance(format!( - "Not enough balance to complete the transaction, current_balance: {current_balance}, required: {required_total_fee}" - ))); - }; - - // build, sign and broadcast assert-commit txns - let mut has_pending_fee_input = false; - for (i, (origin_index, assert_commit_txin, _assert_commit_input_amount)) in - pending_assert_commit_txins.into_iter().enumerate() - { - let fee_inputs = &utxo_sets[i]; - let fee_inputs_total = fee_inputs.iter().map(|input| input.amount).sum::(); - let mut current_has_pending_fee_input = false; - for inputs in fee_inputs.iter() { - if !tx_confirmed(btc_client, &inputs.outpoint.txid).await? { - current_has_pending_fee_input = true; - break; - } - } - if current_has_pending_fee_input { - has_pending_fee_input = true; - continue; - } - - let mut tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], - output: vec![], - }; - tx.input.push(assert_commit_txin); - for input in fee_inputs { - tx.input.push(TxIn { - previous_output: input.outpoint, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::default(), - }); - } - - let fee = required_fees[i]; - let change_value = fee_inputs_total - fee; - if change_value > Amount::from_sat(DUST_AMOUNT) { - tx.output - .push(TxOut { value: change_value, script_pubkey: node_address.script_pubkey() }); - } else { - let op_return_script = generate_opreturn_script( - format!("assert-commit-{origin_index}").as_bytes().to_vec(), - ); - tx.output.push(TxOut { value: Amount::ZERO, script_pubkey: op_return_script }); - } - - for (fee_index, fee_input) in fee_inputs.iter().enumerate() { - let input_index = 1 + fee_index; - node_sign( - &mut tx, - input_index, - fee_input.amount, - EcdsaSighashType::All, - &node_keypair, - )?; - } - - broadcast_tx(btc_client, &tx).await?; - } - - if !has_pending_fee_input && let Err(err) = cleanup_assert_commit_cache(graph_id) { - warn!("failed to cleanup assert-commit cache for graph_id:{graph_id}: {err:?}"); - } - - Ok((None, has_pending_fee_input, None)) -} - -pub async fn send_challenge_tx(btc_client: &BTCClient, graph: &Bitvm2Graph) -> Result { +pub async fn send_challenge_tx(btc_client: &BTCClient, graph: &BitvmGcGraph) -> Result { let (mut challenge_tx, _) = export_challenge_tx(graph)?; - let challenge_keypair = ChallengerMasterKey::new(get_bitvm_key()?).master_keypair(); - let challenger_evm_address = get_node_goat_address() + let challenge_keypair = VerifierMasterKey::new(get_bitvm_key()?).master_keypair(); + let verifier_evm_address = get_node_goat_address() .ok_or_else(|| anyhow::anyhow!("failed to get node goat address".to_string()))?; challenge_tx.output.push(bitcoin::TxOut { value: Amount::ZERO, - script_pubkey: generate_opreturn_script(challenger_evm_address.to_vec()), + script_pubkey: generate_opreturn_script(verifier_evm_address.to_vec()), }); + let connector_a_input = graph + .kickoff + .connector_a_input() + .map_err(|e| anyhow!("failed to get connector-a input: {e}"))?; build_sign_and_broadcast_tx( btc_client, challenge_keypair, challenge_tx.input, - graph.kickoff.tx().output[0].value, + connector_a_input.amount, challenge_tx.output, ) .await @@ -3059,7 +2973,7 @@ pub async fn send_challenge_tx(btc_client: &BTCClient, graph: &Bitvm2Graph) -> R pub async fn send_watchtower_challenge_tx( btc_client: &BTCClient, - graph: &Bitvm2Graph, + graph: &BitvmGcGraph, watchtower_index: usize, commitment_data: Vec, ) -> Result { @@ -3114,7 +3028,7 @@ pub async fn send_watchtower_challenge_tx( } } -pub async fn endorse_graph(goat_client: &GOATClient, graph: &Bitvm2Graph) -> Result { +pub async fn endorse_graph(goat_client: &GOATClient, graph: &BitvmGcGraph) -> Result { let signer = PrivateKeySigner::from_str(&get_node_goat_private_key()?)?; let graph_digest = get_graph_digest(goat_client, graph).await?; let sig = signer.sign_hash(&graph_digest.into()).await?; @@ -3135,7 +3049,7 @@ pub async fn endorse_pegin( pub async fn verify_graph_endorsement( goat_client: &GOATClient, evm_address: &EvmAddress, - graph: &Bitvm2Graph, + graph: &BitvmGcGraph, signature: &[u8], ) -> Result { let graph_digest = get_graph_digest(goat_client, graph).await?; @@ -3221,10 +3135,7 @@ pub async fn upsert_message( let message_id = generate_message_id(business_id, msg_type.to_string().clone(), sub_type); if is_update || storage_processor.find_messages_by_id(&message_id).await?.is_none() { if let Some(cancel_msg_type) = match msg_type { - MessageType::WatchtowerChallengeTimeout => { - Some(MessageType::WatchtowerChallengeInitSent) - } - MessageType::AssertCommitTimeout => Some(MessageType::AssertInitReady), + MessageType::AssertSent => Some(MessageType::WatchtowerChallengeInitSent), _ => None, } { notify_to_cancel_proof_task(storage_processor, business_id, cancel_msg_type).await?; @@ -3256,8 +3167,8 @@ pub async fn notify_to_cancel_proof_task( business_id: Uuid, msg_type: MessageType, ) -> Result<()> { - if !matches!(msg_type, MessageType::WatchtowerChallengeInitSent | MessageType::AssertInitReady) - { + // AssertInitSent is removed; update related logic if needed; + if !matches!(msg_type, MessageType::WatchtowerChallengeInitSent) { warn!("notify_to_cancel_proof_task: input wrong message type:{msg_type}"); return Ok(()); } @@ -3301,20 +3212,6 @@ pub async fn notify_to_cancel_proof_task( info!("call {}, response:{:?}", PROOFS_WATCHTOWER_PROOF_TIMEOUT, response); response.data.is_some() } - MessageType::AssertInitReady => { - let url = host.join(PROOFS_OPERATOR_PROOF_TIMEOUT)?; - let response = http_client - .post_response_json::( - url.as_str(), - &OperatorProofTimeoutUpdateRequest { - instance_id: graph.instance_id.to_string(), - graph_id: graph.graph_id.to_string(), - }, - ) - .await?; - info!("call {}, response:{:?}", PROOFS_OPERATOR_PROOF_TIMEOUT, response); - response.data.is_some() - } _ => false, }; if notify_result { @@ -3336,11 +3233,11 @@ pub async fn notify_to_cancel_proof_task( } /// store new graph, graph_raw_data, and update instance_id -pub async fn get_bitvm2_graph_from_db( +pub async fn get_bitvm_graph_from_db( _local_db: &LocalDB, _instance_id: Uuid, graph_id: Uuid, -) -> Result { +) -> Result { Err(anyhow!("graph:{graph_id} not found")) } @@ -3559,7 +3456,7 @@ pub fn strip_hex_prefix_owned(s: &str) -> String { /// Retrieve the server's public IP via NAT protocol and combine it with /// the configured RPC monitoring port`rpc_addr` to generate the external RPC service address. pub async fn set_node_external_socket_addr_env(rpc_addr: &str) -> Result<()> { - if get_proof_server_url().is_some() { + if get_proof_server_url().is_some() || std::env::var(ENV_EXTERNAL_SOCKET_ADDR).is_ok() { // not provide proof server return Ok(()); } @@ -3687,7 +3584,7 @@ pub async fn get_current_prekickoff_tx( Ok(Some(( (graphs[0].kickoff_index + 1) as u64, - Bitvm2Graph::from_simplified(&simplified_graph)?.next_prekickoff, + BitvmGcGraph::from_simplified(&simplified_graph)?.next_prekickoff, ))) } else { Ok(None) @@ -3779,7 +3676,7 @@ pub async fn store_pegin_request( pub async fn store_instance_parameters( local_db: &LocalDB, - instance_params: &Bitvm2InstanceParameters, + instance_params: &BitvmGcInstanceParameters, ) -> Result<()> { let mut storage_processor = local_db.acquire().await?; storage_processor @@ -3793,7 +3690,7 @@ pub async fn store_instance_parameters( pub async fn get_instance_parameters( local_db: &LocalDB, instance_id: Uuid, -) -> Result> { +) -> Result> { let mut storage_processor = local_db.acquire().await?; if let Some(instance) = storage_processor.find_instance(&instance_id).await? { Ok(if let Some(parameters) = instance.parameters { @@ -3806,60 +3703,64 @@ pub async fn get_instance_parameters( } } -fn convert_graph(bitvm2_graph: &Bitvm2Graph, current_time: i64) -> Graph { +fn convert_graph(bitvm_graph: &BitvmGcGraph, current_time: i64) -> Graph { let mut status = GraphStatus::OperatorPresigned.to_string(); - if bitvm2_graph.committee_pre_signed() { + if bitvm_graph.committee_pre_signed() { status = GraphStatus::CommitteePresigned.to_string(); } Graph { - graph_id: bitvm2_graph.parameters.graph_id, - instance_id: bitvm2_graph.parameters.instance_parameters.instance_id, - kickoff_index: bitvm2_graph.parameters.graph_nonce as i64, + graph_id: bitvm_graph.parameters.graph_id, + instance_id: bitvm_graph.parameters.instance_parameters.instance_id, + kickoff_index: bitvm_graph.parameters.graph_nonce as i64, from_addr: "".to_string(), to_addr: "".to_string(), - amount: bitvm2_graph.parameters.instance_parameters.pegin_amount.to_sat() as i64, - challenge_amount: bitvm2_graph.parameters.challenge_amount.to_sat() as i64, + amount: bitvm_graph.parameters.instance_parameters.pegin_amount.to_sat() as i64, + challenge_amount: bitvm_graph.parameters.challenge_amount.to_sat() as i64, status, sub_status: "".to_string(), - operator_pubkey: bitvm2_graph.parameters.operator_pubkey.to_string(), - cur_prekickoff_txid: Some(bitvm2_graph.cur_prekickoff.finalize().compute_txid().into()), - next_prekickoff: Some(bitvm2_graph.next_prekickoff.finalize().compute_txid().into()), + operator_pubkey: bitvm_graph.parameters.operator_pubkey.to_string(), + cur_prekickoff_txid: Some(bitvm_graph.cur_prekickoff.finalize().compute_txid().into()), + next_prekickoff: Some(bitvm_graph.next_prekickoff.finalize().compute_txid().into()), force_skip_kickoff_txid: Some( - bitvm2_graph.force_skip_kickoff.finalize().compute_txid().into(), + bitvm_graph.force_skip_kickoff.finalize().compute_txid().into(), ), - quick_challenge_txid: Some(bitvm2_graph.quick_challenge.finalize().compute_txid().into()), + quick_challenge_txid: Some(bitvm_graph.quick_challenge.finalize().compute_txid().into()), challenge_incomplete_kickoff_txid: Some( - bitvm2_graph.challenge_incomplete_kickoff.finalize().compute_txid().into(), + bitvm_graph.challenge_incomplete_kickoff.finalize().compute_txid().into(), ), - pegin_txid: Some(bitvm2_graph.pegin.finalize().compute_txid().into()), - kickoff_txid: Some(bitvm2_graph.kickoff.finalize().compute_txid().into()), - take1_txid: Some(bitvm2_graph.take1.finalize().compute_txid().into()), + pegin_txid: Some(bitvm_graph.pegin.finalize().compute_txid().into()), + kickoff_txid: Some(bitvm_graph.kickoff.finalize().compute_txid().into()), + take1_txid: Some(bitvm_graph.take1.finalize().compute_txid().into()), challenge_txid: None, - take2_txid: Some(bitvm2_graph.take2.finalize().compute_txid().into()), - disprove_txid: None, + take2_txid: Some(bitvm_graph.take2.finalize().compute_txid().into()), watchtower_challenge_init_txid: Some( - bitvm2_graph.watchtower_challenge_init.finalize().compute_txid().into(), + bitvm_graph.watchtower_challenge_init.finalize().compute_txid().into(), ), - watchtower_challenge_timeout_txids: bitvm2_graph - .watchtower_challenge_timeout_txns + operator_assert_txid: Some(bitvm_graph.operator_assert.finalize().compute_txid().into()), + verifier_assert_txids: bitvm_graph + .verifier_asserts .iter() .map(|tx| tx.finalize().compute_txid().into()) .collect(), - nack_txids: bitvm2_graph - .nack_txns + disprove_txids: bitvm_graph + .disproves .iter() .map(|tx| tx.finalize().compute_txid().into()) .collect(), - blockhash_commit_timeout_txid: Some( - bitvm2_graph.blockhash_commit_timeout.finalize().compute_txid().into(), - ), - assert_init_txid: Some(bitvm2_graph.assert_init.finalize().compute_txid().into()), - assert_commit_timeout_txids: bitvm2_graph - .assert_commit_timeout_txns + watchtower_challenge_timeout_txids: bitvm_graph + .watchtower_challenge_timeouts + .iter() + .map(|tx| tx.finalize().compute_txid().into()) + .collect(), + operator_challenge_nack_txids: bitvm_graph + .operator_challenge_nacks .iter() .map(|tx| tx.finalize().compute_txid().into()) .collect(), + operator_commit_timeout_txid: Some( + bitvm_graph.operator_commit_timeout.finalize().compute_txid().into(), + ), init_withdraw_tx_hash: None, bridge_out_start_at: 0, status_updated_at: current_time, @@ -3869,24 +3770,24 @@ fn convert_graph(bitvm2_graph: &Bitvm2Graph, current_time: i64) -> Graph { } } -pub async fn store_graph(local_db: &LocalDB, simple_graph: &SimplifiedBitvm2Graph) -> Result<()> { +pub async fn store_graph(local_db: &LocalDB, simple_graph: &SimplifiedBitvmGcGraph) -> Result<()> { let mut tx = local_db.start_transaction().await?; - let bitvm2_graph: Bitvm2Graph = Bitvm2Graph::from_simplified(simple_graph)?; + let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(simple_graph)?; let graph_id = simple_graph.parameters.graph_id; let instance_id = simple_graph.parameters.instance_parameters.instance_id; let current_time = current_time_secs(); - let mut graph = convert_graph(&bitvm2_graph, current_time); + let mut graph = convert_graph(&bitvm_graph, current_time); if let Some(node_info) = - tx.get_node_by_btc_pub_key(&bitvm2_graph.parameters.operator_pubkey.to_string()).await? + tx.get_node_by_btc_pub_key(&bitvm_graph.parameters.operator_pubkey.to_string()).await? { graph.from_addr = node_info.goat_addr.clone(); graph.to_addr = - node_p2wsh_address(get_network(), &bitvm2_graph.parameters.operator_pubkey).to_string(); + node_p2wsh_address(get_network(), &bitvm_graph.parameters.operator_pubkey).to_string(); } tx.upsert_graph(&graph).await?; - if bitvm2_graph.committee_pre_signed() { + if bitvm_graph.committee_pre_signed() { tx.update_instance( &InstanceUpdate::new_with_instance_id(instance_id) .with_status(InstanceBridgeInStatus::Presigned.to_string()), @@ -3907,16 +3808,16 @@ pub async fn store_graph(local_db: &LocalDB, simple_graph: &SimplifiedBitvm2Grap Ok(()) } -/// Parse raw graph data JSON string to SimplifiedBitvm2Graph using spawn_blocking +/// Parse raw graph data JSON string to SimplifiedBitvmGcGraph using spawn_blocking /// to handle large data and potential stack overflow issues pub async fn parse_graph_raw_data( raw_data: String, graph_id: Uuid, -) -> Result { +) -> Result { let raw_data_len = raw_data.len(); let raw_data_clone = raw_data.clone(); let parse_result = tokio::task::spawn_blocking(move || { - serde_json::from_str::(&raw_data_clone) + serde_json::from_str::(&raw_data_clone) }) .await; @@ -3946,10 +3847,10 @@ pub async fn parse_graph_raw_data( } } -/// Serialize SimplifiedBitvm2Graph to JSON string using spawn_blocking +/// Serialize SimplifiedBitvmGcGraph to JSON string using spawn_blocking /// to handle large data and potential stack overflow issues pub async fn serialize_graph_raw_data( - graph: &SimplifiedBitvm2Graph, + graph: &SimplifiedBitvmGcGraph, graph_id: Uuid, ) -> Result { let graph_clone = graph.clone(); @@ -3984,7 +3885,7 @@ pub async fn get_graph( local_db: &LocalDB, _instance_id: Uuid, graph_id: Uuid, -) -> Result> { +) -> Result> { let mut storage_process = local_db.acquire().await?; if let Some(graph_raw_data) = storage_process.find_graph_raw_data(&graph_id).await? && let Ok(simplified_graph) = parse_graph_raw_data(graph_raw_data.raw_data, graph_id).await @@ -3999,7 +3900,7 @@ pub async fn get_graph_by_instance_id_and_operator_pubkey( local_db: &LocalDB, instance_id: Uuid, operator_pubkey: &PublicKey, -) -> Result> { +) -> Result> { let mut storage_process = local_db.acquire().await?; if let Some(graph_id) = storage_process .get_graph_id_by_instance_id_and_operator_pubkey(&instance_id, &operator_pubkey.to_string()) @@ -4480,8 +4381,8 @@ pub async fn update_graph_status( match storage_processor.find_graph(&graph_id).await? { Some(graph) => { if graph.status == new_status.to_string() - && let Some(sub_status) = sub_status - && sub_status == ChallengeSubStatus::default() + && let Some(ref sub_status) = sub_status + && *sub_status == ChallengeSubStatus::default() { warn!( "graph: {graph_id}, new_status: {new_status} is equal old status and ChallengeSubStatus is None, so not update" @@ -4523,7 +4424,7 @@ pub async fn get_graph_ids_for_instance( pub fn gen_instance_parameters_local( instance: &Instance, -) -> anyhow::Result { +) -> anyhow::Result { let network = Network::from_str(&instance.network)?; let committee_pubkeys: Vec = instance .committees_answers @@ -4533,7 +4434,7 @@ pub fn gen_instance_parameters_local( let committee_agg_pubkey = generate_n_of_n_public_key(&committee_pubkeys).0; let utxos: Vec = serde_json::from_str(&instance.input_utxos)?; - Ok(Bitvm2InstanceParameters { + Ok(BitvmGcInstanceParameters { network, instance_id: instance.instance_id, user_info: gen_user_info( @@ -4639,7 +4540,7 @@ pub(crate) async fn get_bridge_out_global_stats<'a>( } pub async fn get_largest_watchtower_challenge_block( - graph: &Bitvm2Graph, + graph: &BitvmGcGraph, btc_client: &BTCClient, ) -> anyhow::Result { let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); @@ -4647,11 +4548,13 @@ pub async fn get_largest_watchtower_challenge_block( let mut largest_watchtower_challenge_block_hash: BlockHash = BlockHash::from_slice(&[0u8; 32]).unwrap(); for watchtower_index in 0..graph.parameters.watchtower_pubkeys.len() { - let watchtower_challenge_vout = 2 * watchtower_index as u32; + let watchtower_challenge_vout = + output_topology::watchtower_challenge_init::watchtower_connector(watchtower_index) + as u64; match outpoint_spent_txid( btc_client, &watchtower_challenge_init_txid, - watchtower_challenge_vout as u64, + watchtower_challenge_vout, ) .await { @@ -4686,165 +4589,187 @@ pub async fn get_largest_watchtower_challenge_block( Ok(largest_watchtower_challenge_block_hash) } -#[cfg(test)] -mod tests { - use super::*; - use std::time::Duration; - - async fn build_large_nonstandard_tx_for_debug( - btc_client: &BTCClient, - node_keypair: &Keypair, - payload_size: usize, - ) -> Result { - let mut msg = "large_opreturn_msg".to_string().as_bytes().to_vec(); - msg.extend(vec![0u8; payload_size - msg.len()]); - let scr = script! { - OP_RETURN - {msg} - } - .compile(); - let mut tx = Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![], - output: vec![TxOut { value: Amount::ZERO, script_pubkey: scr }], - }; - - let node_pubkey: PublicKey = node_keypair.public_key().into(); - let node_address = node_p2wsh_address(get_network(), &node_pubkey); - - let (inputs, _, change_amount) = get_proper_utxo_set( - btc_client, - tx.weight().to_vbytes_ceil(), - node_address.clone(), - Amount::ZERO, - 1.0, - ) - .await? - .ok_or_else(|| { - anyhow!("insufficient UTXOs on {} for debug nonstandard tx test", node_address) - })?; - - for input in &inputs { - tx.input.push(TxIn { - previous_output: input.outpoint, - script_sig: ScriptBuf::new(), - sequence: Sequence::MAX, - witness: Witness::default(), - }); - } - - if change_amount > Amount::from_sat(DUST_AMOUNT) { - tx.output - .push(TxOut { script_pubkey: node_address.script_pubkey(), value: change_amount }); - } +pub(crate) fn babe_setup_state_root(local_db: &LocalDB) -> PathBuf { + std::env::var_os(ENV_BABE_SETUP_STATE_DIR) + .filter(|path| !path.as_os_str().is_empty()) + .map(PathBuf::from) + .unwrap_or_else(|| { + let db_path = local_db + .path + .strip_prefix("sqlite://") + .or_else(|| local_db.path.strip_prefix("sqlite:")) + .unwrap_or(&local_db.path); + PathBuf::from(db_path) + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from(".")) + .join(".bitvm-babe-state") + }) +} - for (i, input) in inputs.iter().enumerate() { - node_sign(&mut tx, i, input.amount, EcdsaSighashType::All, node_keypair)?; - } +fn babe_setup_state_path(local_db: &LocalDB, instance_id: Uuid, graph_id: Uuid) -> PathBuf { + babe_setup_state_root(local_db).join(instance_id.to_string()).join(format!("{graph_id}.json")) +} - Ok(tx) - } +pub(crate) fn soldering_payload_hash(payload: &[u8]) -> [u8; 32] { + Sha256::digest(payload).into() +} - #[tokio::test] - #[ignore] - async fn debug_test_broadcast_nonstandard_tx_and_check_mempool_visibility() -> Result<()> { - if std::env::var(ENV_BITVM_SECRET).is_err() { - bail!("BITVM_SECRET is required and should correspond to a funded testnet key"); - } +pub(crate) fn soldering_payload_hash_hex(payload_hash: &[u8; 32]) -> String { + payload_hash.iter().map(|byte| format!("{byte:02x}")).collect() +} - let network = get_network(); - if network != Network::Testnet4 { - bail!("refuse to run this debug test on non-testnet4 network: {network}"); - } +pub(crate) async fn pending_graph_belongs_to_operator( + local_db: &LocalDB, + instance_id: Uuid, + graph_id: Uuid, + operator_pubkey: &PublicKey, +) -> Result { + let mut storage = local_db.acquire().await?; + Ok(storage.find_pending_graph_init_by_graph_id(&graph_id).await?.is_some_and(|pending| { + pending.instance_id == instance_id && pending.operator_pubkey == operator_pubkey.to_string() + })) +} - let btc_url = - get_btc_url_from_env().unwrap_or_else(|| "https://mempool.space/testnet4".to_string()); - let btc_client = BTCClient::new(network, Some(&btc_url)); - let node_keypair = get_bitvm_key()?; - - // Use an oversized OP_RETURN payload to make tx non-standard for regular mempool relay. - let tx = build_large_nonstandard_tx_for_debug(&btc_client, &node_keypair, 200_000).await?; - let txid = tx.compute_txid(); - println!( - "debug nonstandard tx built: txid={}, vbytes={}", - txid, - tx.weight().to_vbytes_ceil() +#[allow(clippy::too_many_arguments)] +pub(crate) async fn send_soldering_proof_to_operator( + swarm: &mut Swarm, + instance_id: Uuid, + graph_id: Uuid, + verifier_index: usize, + opened: &[(usize, u64)], + finalized: &[FinalizedInstanceData], + soldering: &SolderingData, +) -> Result<()> { + let compact_payload = compact_soldering_proof_payload(opened, finalized, soldering)?; + let payload = bincode::serialize(&compact_payload) + .context("serialize compact soldering proof payload")?; + let payload_hash = soldering_payload_hash(&payload); + let total_len = payload.len(); + let store_base_path = get_soldering_proof_payload_store_path()?; + let payload_path = soldering_proof_payload_store_path( + &store_base_path, + instance_id, + graph_id, + verifier_index, + &payload_hash, + )?; + let store_mode = if is_soldering_proof_s3_path(&store_base_path) { "s3" } else { "local" }; + if let Err(err) = + write_soldering_proof_store_payload(&payload_path, &payload).await.with_context(|| { + format!("write soldering proof payload to configured store path {payload_path}") + }) + { + tracing::error!( + store_mode, + total_len, + payload_hash = %soldering_payload_hash_hex(&payload_hash), + payload_path = %payload_path, + error = %err, + "failed to write soldering proof payload to store" ); + return Err(err); + } + tracing::info!( + store_mode, + total_len, + payload_hash = %soldering_payload_hash_hex(&payload_hash), + payload_path = %payload_path, + "send compact soldering proof ready from payload store" + ); + let message_content = GOATMessageContent::SolderingProofReady(SolderingProofReady { + instance_id, + graph_id, + verifier_index, + payload_hash, + total_len, + }); + send_to_peer(swarm, GOATMessage::new(Actor::Operator, message_content)).await?; - let normal_err = broadcast_tx(&btc_client, &tx).await.err().ok_or_else(|| { - anyhow!("expected normal mempool broadcast to fail for non-standard tx") - })?; - println!("normal broadcast failed as expected: {normal_err:#}"); - - broadcast_nonstandard_tx(&btc_client, &tx).await?; - println!("broadcast_nonstandard_tx succeeded for non-standard tx"); - - let mut found_in_mempool = false; - for _ in 0..10 { - if btc_client.get_tx(&txid).await?.is_some() { - found_in_mempool = true; - break; - } - tokio::time::sleep(Duration::from_secs(1)).await; - } - - if found_in_mempool { - println!( - "successfully found non-standard tx in mempool after broadcast_nonstandard_tx" - ); - } else { - println!("failed to find non-standard tx in mempool after broadcast_nonstandard_tx"); - } + Ok(()) +} - Ok(()) +fn load_babe_setup_state_from_path(path: &Path) -> Result> { + match std::fs::read(path) { + Ok(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(err).with_context(|| format!("read BABE setup state {}", path.display())), } +} - #[tokio::test] - #[ignore = "test on regtest"] - async fn test_get_watchtower_challenge_info() { - let init_txid = - Txid::from_str("2bb03cb075c95d94c298d139242bcd42c366b7105df34591e77e3ac11ac29386") - .unwrap(); - let init_txid = SerializableTxid(init_txid); - let number_challenge = 2; - let esplora_url = "http://localhost:13002".to_string(); - let btc_client = BTCClient::new(get_network(), Some(&esplora_url)); - - let result = - get_watchtower_challenge_info(&btc_client, &init_txid, number_challenge).await.unwrap(); - println!("result: {result:#?}"); +fn save_babe_setup_state_to_path(path: &Path, state: &BabeSetupState) -> Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent) + .with_context(|| format!("create BABE setup state dir {}", parent.display()))?; } + let bytes = serde_json::to_vec_pretty(state)?; + std::fs::write(path, bytes) + .with_context(|| format!("write BABE setup state {}", path.display())) +} - #[test] - fn test_operator_proof_request_url() { - unsafe { - std::env::set_var(ENV_PROOF_BUILD_URL, "http://127.0.0.1:8900"); - } - - let base_url = Url::parse( - &get_proof_build_rpc_host() - .ok_or_else(|| anyhow::anyhow!("failed to get proof_build_rpc_host")) - .unwrap(), - ) - .unwrap(); - unsafe { - std::env::remove_var(ENV_PROOF_BUILD_URL); - } - let url = base_url.join(NODES_OPERATOR_BASE).unwrap(); - - assert_eq!(url.as_str(), "http://127.0.0.1:8900/v1/proofs/operator_proofs"); - } +pub(crate) fn load_babe_setup_state( + local_db: &LocalDB, + instance_id: Uuid, + graph_id: Uuid, +) -> Result> { + load_babe_setup_state_from_path(&babe_setup_state_path(local_db, instance_id, graph_id)) +} - #[test] - fn test_load_part_stark_vk_for_zkm_version_accepts_known_version() { - let part_stark_vk = load_part_stark_vk_for_zkm_version("v1.2.4").unwrap(); - assert!(!part_stark_vk.is_empty()); - } +pub(crate) fn save_babe_setup_state( + local_db: &LocalDB, + instance_id: Uuid, + graph_id: Uuid, + state: &BabeSetupState, +) -> Result<()> { + save_babe_setup_state_to_path(&babe_setup_state_path(local_db, instance_id, graph_id), state) +} - #[test] - fn test_load_part_stark_vk_for_zkm_version_rejects_unknown_version_without_panic() { - let err = load_part_stark_vk_for_zkm_version("v0.0.0-test").unwrap_err(); - assert!(err.to_string().contains("failed to load part_stark_vk")); - } +pub(crate) fn update_babe_setup_state( + local_db: &LocalDB, + instance_id: Uuid, + graph_id: Uuid, + update: impl FnOnce(&mut BabeSetupState), +) -> Result { + let mut state = load_babe_setup_state(local_db, instance_id, graph_id)?.unwrap_or_default(); + update(&mut state); + save_babe_setup_state(local_db, instance_id, graph_id, &state)?; + Ok(state) +} + +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct BabeSetupState { + pub verifier: Option, + pub operator: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct VerifierBabeSetupState { + pub verifier_pubkey: PublicKey, + pub setup_package: CACSetupPackage, + pub private_state: BabeVerifierPrivateState, + pub verifier_index: Option, + pub finalized_indices: Vec, + pub opened: Vec<(usize, u64)>, + pub finalized: Vec, + pub soldering: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct OperatorVerifierCandidate { + pub verifier_pubkey: PublicKey, + pub setup_package: CACSetupPackage, + pub verifier_index: Option, + pub selected_circuit_indexes: Vec, + pub gc_data: Option, + #[serde(default)] + pub prover_state: Option, +} + +#[derive(Clone, Serialize, Deserialize)] +pub struct OperatorBabeSetupState { + pub frozen_verifier_pubkeys: Option>, + pub candidates: Vec, + #[serde(default)] + pub asserted_wrapper_proof: Option>, } diff --git a/node/src/vk.rs b/node/src/vk.rs index 708b3b6b..5e5667ce 100644 --- a/node/src/vk.rs +++ b/node/src/vk.rs @@ -1,5 +1,6 @@ +#![allow(dead_code)] + use anyhow::Result; -use bitvm2_lib::types::VerifyingKey; use std::fs; use std::path::PathBuf; use zkm_sdk::install::CIRCUIT_ARTIFACTS_URL_BASE; @@ -11,6 +12,8 @@ use { std::{cmp::min, process::Command}, }; +pub type VerifyingKey = ark_groth16::VerifyingKey; + pub(crate) fn block_on(fut: impl std::future::Future) -> T { use tokio::task::block_in_place; diff --git a/node/ssp-ci.sh b/node/ssp-ci.sh index 7aa3c259..90f035e7 100644 --- a/node/ssp-ci.sh +++ b/node/ssp-ci.sh @@ -26,16 +26,16 @@ $CMD fund $CMD payfee --total 5 echo -e "publish genisis sign sequencer set" -$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --init-genesis --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.0" +$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH --init-genesis --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.0" echo -e "set the new publisher set: publishers: ${PUBLISHER_BTC_PUBKEYS} => next_publishers: ${NEXT_PUBLISHER_BTC_PUBKEYS}" $CMD payfee --total 5 -$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER -$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER -$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER -$CMD sign-seq --owner-btc-key-wif cMiWPrRA5KYDiRAq4nkgGsEf2TfcpqGbhT6YbfDpoy8ZsaAHiDeo --goat-block-number $GOAT_BLOCK_NUMBER -$CMD sign-seq --owner-btc-key-wif cMkTafzStDS4RMRPYD7Emw9DfN5Yendp9R9eKBaNg7tBWwGU43fD --goat-block-number $GOAT_BLOCK_NUMBER +$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMiWPrRA5KYDiRAq4nkgGsEf2TfcpqGbhT6YbfDpoy8ZsaAHiDeo --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMkTafzStDS4RMRPYD7Emw9DfN5Yendp9R9eKBaNg7tBWwGU43fD --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH # broadcast publisher changes to Bitcoin $CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.1" @@ -44,10 +44,10 @@ GOAT_BLOCK_NUMBER=$(($GOAT_BLOCK_NUMBER+1)) echo -e "recover the publisher set: next_publishers => publishers" $CMD payfee --total 3 -$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS -$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS -$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS +$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH # broadcast publisher changes to Bitcoin -$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.2" +$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.2" diff --git a/proof-builder-rpc/Cargo.toml b/proof-builder-rpc/Cargo.toml index bec395f7..68e9b5b3 100644 --- a/proof-builder-rpc/Cargo.toml +++ b/proof-builder-rpc/Cargo.toml @@ -8,6 +8,7 @@ name = "proof-builder-rpc" path = "src/main.rs" [dependencies] +zkm-sdk = { workspace = true } clap = { workspace = true } futures = { workspace = true } dotenv = { workspace = true } @@ -35,6 +36,7 @@ commit-chain = { workspace = true } state-chain-proof = { workspace = true } watchtower-proof = { workspace = true } operator-proof = { workspace = true } +operator-wrapper-proof = { workspace = true } util = { workspace = true } client = { workspace = true } bitcoin-light-client-circuit.workspace = true diff --git a/proof-builder-rpc/README.md b/proof-builder-rpc/README.md index 3d79be87..afcb67ec 100644 --- a/proof-builder-rpc/README.md +++ b/proof-builder-rpc/README.md @@ -26,7 +26,7 @@ Long-running proof tasks (stored in the `long_running_task_proof` table) — suc GOAT_BLOCK_NUMBER=${THE_GOAT_BLOCK_NUMBER} bash -x scp.sh ``` -Example `scp.sh` used for Regtest integration tests: +Example `scp.sh` used for Regtest integration tests, remember to set the `OPERATOR_VK_HASH` environment variable before running: ```bash #!/bin/bash diff --git a/proof-builder-rpc/proof-builder.toml.example b/proof-builder-rpc/proof-builder.toml.example index 96dce504..c90145c6 100644 --- a/proof-builder-rpc/proof-builder.toml.example +++ b/proof-builder-rpc/proof-builder.toml.example @@ -73,3 +73,9 @@ graph_id="86F23E650F6D447885CA320E6BA27ABD" execution_layer_block_number = 10321027 index = 1 bitcoin_network = "regtest" + +[wrapper] +enable = false +output = "../circuits/data/wrapper/output.bin" +genesis_sequencer_commit_txid = "5d5a29e1c426abc4b947f90b93c6b043683aafad79cb3b9a9829dfd5e51cad09" +scan_interval = 30 diff --git a/proof-builder-rpc/src/api/mod.rs b/proof-builder-rpc/src/api/mod.rs index 8ad9cfa0..c1f5d999 100644 --- a/proof-builder-rpc/src/api/mod.rs +++ b/proof-builder-rpc/src/api/mod.rs @@ -6,9 +6,9 @@ mod validation; use crate::api::metrics_service::{ApiMetricsState, metrics_handler, metrics_middleware}; use crate::api::proof_handler::{ - get_chain_proof_task_desc, get_operator_proof_task_desc, post_operator_proof_task, - post_watchtower_proof_task, update_operator_proof_task_timeout, - update_watchtower_proof_task_timeout, + get_chain_proof_task_desc, get_operator_proof_task_desc, get_wrapper_proof_task, + get_wrapper_proof_task_desc, post_operator_proof_task, post_watchtower_proof_task, + update_operator_proof_task_timeout, update_watchtower_proof_task_timeout, }; use axum::http::Method; use axum::routing::{get, post}; @@ -48,6 +48,8 @@ pub(crate) async fn serve( .route(routes::v1::PROOFS_OPERATOR_PROOF, post(post_operator_proof_task)) .route(routes::v1::PROOFS_OPERATOR_PROOF_TIMEOUT, post(update_operator_proof_task_timeout)) .route(routes::v1::PROOFS_OPERATOR_PROOF_DESC, get(get_operator_proof_task_desc)) + .route(routes::v1::PROOFS_WRAPPER_PROOF, get(get_wrapper_proof_task)) + .route(routes::v1::PROOFS_WRAPPER_PROOF_DESC, get(get_wrapper_proof_task_desc)) .layer(middleware::from_fn_with_state(api_state.clone(), metrics_middleware)) .layer(CorsLayer::new().allow_headers(Any).allow_origin(Any).allow_methods(vec![ Method::GET, diff --git a/proof-builder-rpc/src/api/proof_handler.rs b/proof-builder-rpc/src/api/proof_handler.rs index 37e72da4..dde156e8 100644 --- a/proof-builder-rpc/src/api/proof_handler.rs +++ b/proof-builder-rpc/src/api/proof_handler.rs @@ -1,20 +1,22 @@ use crate::api::ApiState; -use crate::api::response::{ApiErrorExt, ApiResult, ok_response}; +use crate::api::response::{ApiErrorExt, ApiResult, ErrorResponse, ok_response}; use crate::api::validation::InputValidator; use crate::task::{ - add_operator_task, add_watchtower_task, find_operator_task, find_watchtower_task, - update_operator_task_state, update_watchtower_task_state, + add_operator_task, add_watchtower_task, create_missing_wrapper_tasks, find_operator_task, + find_watchtower_task, update_operator_task_state, update_watchtower_task_state, }; use axum::Json; use axum::extract::{Query, State}; +use axum::http::StatusCode; use proof_builder::{ ChainProofDescRequest, OperatorProofDescRequest, OperatorProofRequest, OperatorProofResponse, OperatorProofTimeoutUpdateRequest, OperatorProofTimeoutUpdateResponse, ProofData, ProofDesc, ProofDescResponse, ProofType, WatchtowerProofRequest, WatchtowerProofResponse, WatchtowerProofTimeoutUpdateRequest, WatchtowerProofTimeoutUpdateResponse, + WrapperProofDescRequest, WrapperProofMetadata, WrapperProofResponse, }; use std::sync::Arc; -use store::ProofState; +use store::{ProofState, WrapperProof}; use tracing::info; #[axum::debug_handler] @@ -185,6 +187,63 @@ pub(super) async fn post_operator_proof_task( } } } + +#[axum::debug_handler] +pub(super) async fn get_wrapper_proof_task( + State(api_state): State>, + Query(payload): Query, +) -> ApiResult { + let wrapper_proof = find_wrapper_proof_by_request(&api_state, &payload).await?; + match wrapper_proof { + Some(wrapper_proof) + if wrapper_proof.proof_state == ProofState::Proven.to_i64() + && wrapper_proof.path_to_proof.is_some() => + { + ok_response(WrapperProofResponse { + proof_data: Some(ProofData::load_proof_data( + wrapper_proof.path_to_proof.as_deref().unwrap(), + ProofType::Wrapper, + )), + metadata: Some(wrapper_metadata(&wrapper_proof)), + error: None, + }) + } + Some(wrapper_proof) => ok_response(WrapperProofResponse { + proof_data: None, + metadata: Some(wrapper_metadata(&wrapper_proof)), + error: Some(format!( + "The wrapper proof is not ready, state {}, path:{:?}", + wrapper_proof.proof_state, wrapper_proof.path_to_proof + )), + }), + None => ok_response(WrapperProofResponse { + proof_data: None, + metadata: None, + error: Some("No wrapper proof found".to_string()), + }), + } +} + +#[axum::debug_handler] +pub(super) async fn get_wrapper_proof_task_desc( + State(api_state): State>, + Query(payload): Query, +) -> ApiResult { + let wrapper_proof = find_wrapper_proof_by_request(&api_state, &payload).await?; + match wrapper_proof { + Some(wrapper_proof) => ok_response(WrapperProofResponse { + proof_data: None, + metadata: Some(wrapper_metadata(&wrapper_proof)), + error: None, + }), + None => ok_response(WrapperProofResponse { + proof_data: None, + metadata: None, + error: Some("No wrapper proof found".to_string()), + }), + } +} + #[axum::debug_handler] pub(super) async fn update_operator_proof_task_timeout( State(api_state): State>, @@ -216,6 +275,78 @@ pub(super) async fn update_operator_proof_task_timeout( } } +async fn find_wrapper_proof_by_request( + api_state: &Arc, + payload: &WrapperProofDescRequest, +) -> Result, (StatusCode, Json)> { + if let Some(operator_proof_id) = payload.operator_proof_id { + let mut storage_process = + api_state.local_db.acquire().await.api_error("GET_WRAPPER_PROOF_ERROR")?; + return storage_process + .find_wrapper_proof_by_operator_proof_id(operator_proof_id) + .await + .api_error("GET_WRAPPER_PROOF_ERROR"); + } + + let instance_id = InputValidator::validate_uuid( + payload.instance_id.as_deref().unwrap_or_default(), + "instance_id", + )?; + let graph_id = + InputValidator::validate_uuid(payload.graph_id.as_deref().unwrap_or_default(), "graph_id")?; + + let wrapper_proof = { + let mut storage_process = + api_state.local_db.acquire().await.api_error("GET_WRAPPER_PROOF_ERROR")?; + storage_process + .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) + .await + .api_error("GET_WRAPPER_PROOF_ERROR")? + }; + + if wrapper_proof.is_some() { + return Ok(wrapper_proof); + } + + if let Some(genesis_txid) = payload.genesis_sequencer_commit_txid.as_deref() + && !genesis_txid.is_empty() + { + create_missing_wrapper_tasks(&api_state.local_db, genesis_txid) + .await + .api_error("GET_WRAPPER_PROOF_ERROR")?; + } + + let mut storage_process = + api_state.local_db.acquire().await.api_error("GET_WRAPPER_PROOF_ERROR")?; + storage_process + .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) + .await + .api_error("GET_WRAPPER_PROOF_ERROR") +} + +fn wrapper_metadata(wrapper_proof: &WrapperProof) -> WrapperProofMetadata { + WrapperProofMetadata { + id: wrapper_proof.id, + operator_proof_id: wrapper_proof.operator_proof_id, + instance_id: wrapper_proof.instance_id.to_string(), + graph_id: wrapper_proof.graph_id.to_string(), + operator_path_to_proof: wrapper_proof.operator_path_to_proof.clone(), + path_to_proof: wrapper_proof.path_to_proof.clone(), + public_value_hex: wrapper_proof.public_value_hex.clone(), + operator_vk_hash: wrapper_proof.operator_vk_hash.clone(), + genesis_sequencer_commit_txid: wrapper_proof.genesis_sequencer_commit_txid.clone(), + operator_public_value_hex: wrapper_proof.operator_public_value_hex.clone(), + proof_state: wrapper_proof.proof_state, + proof_size: wrapper_proof.proof_size, + cycles: wrapper_proof.cycles, + total_time_to_proof: wrapper_proof.total_time_to_proof, + proving_time: wrapper_proof.proving_time, + zkm_version: wrapper_proof.zkm_version.clone(), + created_at: wrapper_proof.created_at, + updated_at: wrapper_proof.updated_at, + } +} + #[axum::debug_handler] pub(super) async fn post_watchtower_proof_task( State(api_state): State>, @@ -226,14 +357,6 @@ pub(super) async fn post_watchtower_proof_task( let challenge_init_txid = InputValidator::validate_btc_txid(&payload.challenge_init_txid, "challenge_init_txid")? .to_string(); - let mut storage_process = - api_state.local_db.acquire().await.api_error("POST_WATCHTOWER_PROOF_TASK_ERROR")?; - storage_process - .find_graph(&graph_id) - .await - .api_error("POST_WATCHTOWER_PROOF_TASK_ERROR")? - .ok_or_else(|| anyhow::anyhow!("graph {graph_id} not found")) - .api_error("POST_WATCHTOWER_PROOF_TASK_ERROR")?; let watchtower_proof = find_watchtower_task(&api_state.local_db, instance_id, graph_id, &payload.public_key) diff --git a/proof-builder-rpc/src/api/routes.rs b/proof-builder-rpc/src/api/routes.rs index fea597ee..df37f6c7 100644 --- a/proof-builder-rpc/src/api/routes.rs +++ b/proof-builder-rpc/src/api/routes.rs @@ -7,4 +7,6 @@ pub(crate) mod v1 { pub(crate) const PROOFS_OPERATOR_PROOF: &str = "/v1/proofs/operator_proofs"; pub(crate) const PROOFS_OPERATOR_PROOF_TIMEOUT: &str = "/v1/proofs/operator_proofs_timeout"; pub(crate) const PROOFS_OPERATOR_PROOF_DESC: &str = "/v1/proofs/operator_proofs_desc"; + pub(crate) const PROOFS_WRAPPER_PROOF: &str = "/v1/proofs/wrapper_proofs"; + pub(crate) const PROOFS_WRAPPER_PROOF_DESC: &str = "/v1/proofs/wrapper_proofs_desc"; } diff --git a/proof-builder-rpc/src/config.rs b/proof-builder-rpc/src/config.rs index 7c137c5b..c47ff1ce 100644 --- a/proof-builder-rpc/src/config.rs +++ b/proof-builder-rpc/src/config.rs @@ -9,6 +9,8 @@ pub(crate) struct ProofBuilderConfig { pub state_chain: state_chain_proof::Args, pub watchtower: watchtower_proof::Args, pub operator: operator_proof::Args, + #[serde(default)] + pub wrapper: operator_wrapper_proof::Args, } impl ProofBuilderConfig { diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index e58fcb03..6241a103 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -3,20 +3,24 @@ mod header_chain_proof; mod operator_proof; mod state_chain_proof; mod watchtower_proof; +mod wrapper_proof; use crate::config::ProofBuilderConfig; use crate::task::{ commit_chain_proof::spawn_commit_chain_proof_task, header_chain_proof::spawn_header_chain_proof_task, operator_proof::spawn_operator_proof_task, state_chain_proof::spawn_state_chain_proof_task, watchtower_proof::spawn_watchtower_proof_task, + wrapper_proof::spawn_wrapper_proof_task, }; use ::commit_chain_proof::CommitChainProofBuilder; use ::header_chain_proof::HeaderChainProofBuilder; use ::state_chain_proof::StateChainProofBuilder; use bitcoin::{BlockHash, Network, Txid}; use client::btc_chain::BTCClient; +use std::collections::HashSet; use std::str::FromStr; use std::time::UNIX_EPOCH; use uuid::Uuid; +pub(crate) use wrapper_proof::create_missing_wrapper_tasks; use futures::future::Either; use proof_builder::{OnDemandTask, ProofBuilder}; @@ -31,6 +35,7 @@ pub(crate) fn is_start_generate_proof_tasks(cfg: &ProofBuilderConfig) -> bool { || cfg.state_chain.enable || cfg.watchtower.enable || cfg.operator.enable + || cfg.wrapper.enable } pub(crate) async fn run_generate_proof_tasks( @@ -99,6 +104,18 @@ pub(crate) async fn run_generate_proof_tasks( Either::Right(std::future::pending::, tokio::task::JoinError>>()) }; + let wrapper_proof_future = if cfg.wrapper.enable { + Either::Left(spawn_wrapper_proof_task( + cfg.wrapper.clone(), + local_db.clone(), + interval, + interval, + cancellation_token.clone(), + )) + } else { + Either::Right(std::future::pending::, tokio::task::JoinError>>()) + }; + tokio::select! { result = header_chain_proof_future => { match result { @@ -175,6 +192,21 @@ pub(crate) async fn run_generate_proof_tasks( } } } + result = wrapper_proof_future => { + match result { + Ok(Ok(_)) => { + info!("Wrapper proof generate task completed successfully"); + } + Ok(Err(e)) => { + error!("Wrapper proof generate task error: {}", e); + return Err(e); + } + Err(e) => { + error!("Wrapper proof generate task panic: {:?}", e); + anyhow::bail!("Wrapper proof generate task panic: {:?}", e); + } + } + } } Ok("tasks_completed".to_string()) @@ -214,7 +246,7 @@ async fn read_watchtower_challenge_details<'a>( i64, i64, Option, - Vec, + Vec>, Vec, Vec, Option, @@ -234,8 +266,8 @@ async fn read_watchtower_challenge_details<'a>( Some(task) => { tracing::info!("watchtower task: {task:?}"); // NOTE: we use watchtower challenge init txid to calculate the height - let watchtower_challenge_txids: Vec = - vec![task.challenge_init_txid.0.to_string()]; + let watchtower_challenge_txids: Vec> = + vec![Some(task.challenge_init_txid.0.to_string())]; let pubkeys: Vec = vec![task.public_key.clone()]; ( task.id, @@ -281,8 +313,10 @@ async fn read_watchtower_challenge_details<'a>( ); } } - let challenge_txids: Vec = - watchtower_info.iter().map(|w| w.challenge_txid.0.to_string()).collect::>(); + let challenge_txids: Vec> = watchtower_info + .iter() + .map(|w| w.included.then(|| w.challenge_txid.0.to_string())) + .collect::>(); let challenge_public_keys: Vec = watchtower_info.iter().map(|w| w.public_key.clone()).collect::>(); let included_watchtowers: Vec = @@ -350,7 +384,7 @@ pub(crate) async fn fetch_on_demand_task( let mut largest_btc_block_height = 0; let btc_client = BTCClient::new(bitcoin_network, Some(esplora_url)); - for txid in &watchtower_challenge_txids { + for txid in watchtower_challenge_txids.iter().flatten() { let txid = Txid::from_str(txid)?; let block_height = match btc_client.get_tx_status(&txid).await { Ok(tx_status) => { @@ -741,18 +775,23 @@ pub(crate) async fn add_operator_task( graph_id: Uuid, operator_committed_blockhash: String, execution_layer_block_number: i64, - watchtower_challenge_txids: Vec, + watchtower_challenge_txids: Vec>, included_watchtowers: Vec, watchtower_challenge_init_txid: String, watchtower_challenge_pubkeys: Vec, ) -> anyhow::Result { + validate_indexed_watchtower_challenges( + &watchtower_challenge_txids, + &included_watchtowers, + &watchtower_challenge_pubkeys, + )?; let mut storage_processor = local_db.start_transaction().await?; let existing_watchtower_proof_task = storage_processor .find_watchtower_proof_by_instance_and_graph(&instance_id, &graph_id) .await?; tracing::info!("existing_watchtower_proof_task: {:?}", existing_watchtower_proof_task); - let timeout_watchtower_challenge_txids: Vec = watchtower_challenge_txids + let missing_watchtower_indices: Vec = watchtower_challenge_txids .iter() .enumerate() .filter(|(node_index, _)| { @@ -760,40 +799,44 @@ pub(crate) async fn add_operator_task( .iter() .any(|task| task.node_index as usize == *node_index) }) - .map(|(_, txid)| txid.clone()) + .map(|(node_index, _)| node_index) .collect(); - tracing::info!("timeout_watchtower_challenge_txids: {:?}", timeout_watchtower_challenge_txids); + tracing::info!("missing_watchtower_indices: {:?}", missing_watchtower_indices); // insert timeout watchtower proof task with failed state. - for txid in timeout_watchtower_challenge_txids.iter() { - let node_index = watchtower_challenge_txids.iter().position(|t| t == txid).unwrap() as i32; + for node_index in missing_watchtower_indices { + let stored_txid = watchtower_challenge_txids[node_index] + .as_deref() + .unwrap_or(&watchtower_challenge_init_txid); let task = WatchtowerProof { id: 1, instance_id, graph_id, challenge_init_txid: Txid::from_str(&watchtower_challenge_init_txid)?.into(), - challenge_txid: Txid::from_str(txid)?.into(), + challenge_txid: Txid::from_str(stored_txid)?.into(), proof_state: ProofState::Failed.to_i64(), created_at: current_time_secs(), updated_at: current_time_secs(), execution_layer_block_number, - public_key: watchtower_challenge_pubkeys[node_index as usize].clone(), // Note: this is a fake public key. - node_index, + public_key: watchtower_challenge_pubkeys[node_index].clone(), // Note: this is a fake public key. + node_index: node_index as i32, + included: included_watchtowers[node_index], ..Default::default() }; let affected = storage_processor.create_watchtower_proof(&task).await?; tracing::info!( - "mark timeout watchtower proof as failed, txid: {txid}, affected rows: {affected}", + "mark timeout watchtower proof as failed, node_index: {node_index}, affected rows: {affected}", ); } // update watchtower's challenge txid. for (i, txid) in watchtower_challenge_txids.iter().enumerate() { + let stored_txid = txid.as_deref().unwrap_or(&watchtower_challenge_init_txid); let affected = storage_processor .update_watchtower_proof_challenge_txid( &instance_id, &graph_id, i as i32, - txid, + stored_txid, included_watchtowers[i], ) .await?; @@ -818,6 +861,39 @@ pub(crate) async fn add_operator_task( Ok(affected_rows) } +fn validate_indexed_watchtower_challenges( + challenge_txids: &[Option], + included_watchtowers: &[bool], + challenge_pubkeys: &[String], +) -> anyhow::Result<()> { + if challenge_txids.len() != included_watchtowers.len() + || challenge_txids.len() != challenge_pubkeys.len() + { + anyhow::bail!( + "watchtower challenge txids, included bitmap, and public keys must have equal lengths" + ); + } + + let mut seen = HashSet::new(); + for (index, (txid, included)) in challenge_txids.iter().zip(included_watchtowers).enumerate() { + match (txid, included) { + (Some(txid), true) => { + Txid::from_str(txid)?; + if !seen.insert(txid) { + anyhow::bail!("duplicate watchtower challenge txid at index {index}"); + } + } + (None, false) => {} + (Some(_), false) | (None, true) => { + anyhow::bail!( + "watchtower challenge txid and included flag disagree at index {index}" + ); + } + } + } + Ok(()) +} + pub(crate) async fn find_operator_task( local_db: &LocalDB, instance_id: Uuid, @@ -919,8 +995,8 @@ mod tests { let graph_id = Uuid::from_str("00112233445566778899aabbccddeeff").unwrap(); let number = 9511055; let watchtower_challenge_txids = vec![ - "4506cf35cd70b3006fe3ce4a87ca1f9b0a76f348cfb529423e2d4c163c28d604".to_string(), - "f16286f143430a229c6d068798cd9ba751e83202de5045cc788aab227114cdb2".to_string(), + Some("4506cf35cd70b3006fe3ce4a87ca1f9b0a76f348cfb529423e2d4c163c28d604".to_string()), + None, ]; let operator_committed_blockhash = "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246".to_string(); diff --git a/proof-builder-rpc/src/task/operator_proof.rs b/proof-builder-rpc/src/task/operator_proof.rs index 6717b063..bf42a9b2 100644 --- a/proof-builder-rpc/src/task/operator_proof.rs +++ b/proof-builder-rpc/src/task/operator_proof.rs @@ -11,6 +11,7 @@ use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tracing::info; use util::hex_parse; +use zkm_sdk::HashableKey; #[tracing::instrument(level = "info", skip(local_db, cancellation_token))] pub(crate) fn spawn_operator_proof_task( @@ -30,6 +31,7 @@ pub(crate) fn spawn_operator_proof_task( } let builder = OperatorProofBuilder::new(); + info!("operator vk hash {:?}", builder.vk().bytes32()); loop { tokio::select! { _ = tokio::time::sleep(Duration::from_secs(interval)) => { @@ -50,8 +52,24 @@ pub(crate) fn spawn_operator_proof_task( args.graph_id ); args.watchtower_challenge_init_txid = next_task.watchtower_challenge_init_txid.unwrap().clone(); - args.watchtower_challenge_txids = next_task.watchtower_challenge_txids.join(","); - args.watchtower_public_keys = next_task.watchtower_public_keys.join(","); + let included_challenges: Vec<_> = next_task + .watchtower_challenge_txids + .iter() + .zip(&next_task.watchtower_public_keys) + .filter_map(|(txid, public_key)| { + txid.as_ref().map(|txid| (txid.as_str(), public_key.as_str())) + }) + .collect(); + args.watchtower_challenge_txids = included_challenges + .iter() + .map(|(txid, _)| *txid) + .collect::>() + .join(","); + args.watchtower_public_keys = included_challenges + .iter() + .map(|(_, public_key)| *public_key) + .collect::>() + .join(","); // LE array to string, e.g. [1, 1, 1, 0] => 7 args.included_watchtowers = le_bits_to_u256(&next_task.included_watchtowers).to_string(); task_index = next_task.task_index; diff --git a/proof-builder-rpc/src/task/wrapper_proof.rs b/proof-builder-rpc/src/task/wrapper_proof.rs new file mode 100644 index 00000000..2078820c --- /dev/null +++ b/proof-builder-rpc/src/task/wrapper_proof.rs @@ -0,0 +1,259 @@ +use crate::task::current_time_secs; +use operator_wrapper_proof::OperatorWrapperProofBuilder; +use proof_builder::{ProofBuilder, ProofRequest}; +use std::path::Path; +use std::time::Duration; +use store::localdb::LocalDB; +use store::{ProofState, WrapperProof}; +use tokio::task::JoinHandle; +use tokio_util::sync::CancellationToken; +use zkm_sdk::HashableKey; + +#[tracing::instrument(level = "info", skip(local_db))] +pub(crate) async fn create_missing_wrapper_tasks( + local_db: &LocalDB, + genesis_sequencer_commit_txid: &str, +) -> anyhow::Result { + let mut storage_processor = local_db.acquire().await?; + let operator_proofs = storage_processor.find_proven_operator_proofs_without_wrapper(32).await?; + let mut created = 0u64; + + for operator_proof in operator_proofs { + let Some(operator_path_to_proof) = operator_proof.path_to_proof.clone() else { + continue; + }; + let operator_vk_hash = + read_utf8_sidecar(&format!("{operator_path_to_proof}.vk_hash.bin")).unwrap_or_default(); + let now = current_time_secs(); + let wrapper_proof = WrapperProof { + operator_proof_id: operator_proof.id, + instance_id: operator_proof.instance_id, + graph_id: operator_proof.graph_id, + execution_layer_block_number: operator_proof.execution_layer_block_number, + operator_path_to_proof, + operator_vk_hash, + genesis_sequencer_commit_txid: genesis_sequencer_commit_txid.to_string(), + operator_public_value_hex: operator_proof.public_value_hex.clone(), + proof_state: ProofState::New.to_i64(), + created_at: now, + updated_at: now, + ..Default::default() + }; + + match storage_processor.create_wrapper_proof(&wrapper_proof).await { + Ok(rows) => created += rows, + Err(err) if err.to_string().contains("UNIQUE") => { + tracing::debug!( + operator_proof_id = operator_proof.id, + "wrapper task already exists" + ); + } + Err(err) => return Err(err), + } + } + + Ok(created) +} + +#[tracing::instrument(level = "info", skip(local_db, cancellation_token))] +pub(crate) fn spawn_wrapper_proof_task( + args: operator_wrapper_proof::Args, + local_db: LocalDB, + interval: u64, + initial_delay: u64, + cancellation_token: CancellationToken, +) -> JoinHandle> { + let args = args.clone(); + tokio::spawn(async move { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(initial_delay)) => {} + _ = cancellation_token.cancelled() => { + anyhow::bail!("Wrapper proof generate task cancelled"); + } + } + + let builder = OperatorWrapperProofBuilder::new(); + tracing::info!("operator wrapper vk hash {:?}", builder.vk().bytes32()); + let scan_interval = if args.scan_interval == 0 { interval } else { args.scan_interval }; + loop { + tokio::select! { + _ = tokio::time::sleep(Duration::from_secs(scan_interval)) => { + if args.genesis_sequencer_commit_txid.is_empty() { + tracing::warn!("Wrapper proof task skipped: genesis_sequencer_commit_txid is empty"); + continue; + } + + let created = create_missing_wrapper_tasks( + &local_db, + &args.genesis_sequencer_commit_txid, + ).await?; + if created > 0 { + tracing::info!("created {created} wrapper proof tasks"); + } + + let wrapper_task = { + let mut storage_processor = local_db.acquire().await?; + storage_processor + .claim_next_wrapper_proof() + .await? + }; + let Some(wrapper_task) = wrapper_task else { + tracing::debug!("No wrapper proof task found"); + continue; + }; + + let output = wrapper_output_path(&args.output, &wrapper_task); + let ctx = ProofRequest::WrapperProofRequest { + operator_proof_id: wrapper_task.operator_proof_id, + operator_input_proof: wrapper_task.operator_path_to_proof.clone(), + graph_id: *wrapper_task.graph_id.as_bytes(), + genesis_sequencer_commit_txid: wrapper_task.genesis_sequencer_commit_txid.clone(), + output: output.clone(), + }; + + let proving_start = tokio::time::Instant::now(); + let result = builder.build_proof(&ctx).and_then(|(input, proof, cycles, proving_time)| { + let zkm_version = proof.zkm_version.clone(); + let (public_value_hex, proof_size) = + builder.save_proof(&ctx, &input, cycles, proof)?; + Ok((cycles, proving_time, public_value_hex, proof_size, zkm_version)) + }); + + match result { + Ok((cycles, proving_time, public_value_hex, proof_size, zkm_version)) => { + let proving_duration = proving_start.elapsed().as_secs_f32() * 1000.0; + let mut storage_processor = local_db.acquire().await?; + let affected = storage_processor + .update_wrapper_proof_success( + wrapper_task.id, + output, + public_value_hex, + proof_size as i64, + cycles as i64, + proving_time as i64, + &zkm_version, + ) + .await?; + tracing::info!( + wrapper_proof_id = wrapper_task.id, + operator_proof_id = wrapper_task.operator_proof_id, + proving_duration_ms = proving_duration as i64, + affected, + "wrapper proof generated" + ); + } + Err(err) => { + let mut storage_processor = local_db.acquire().await?; + storage_processor + .update_wrapper_proof_failure(wrapper_task.id) + .await?; + tracing::warn!( + wrapper_proof_id = wrapper_task.id, + operator_proof_id = wrapper_task.operator_proof_id, + error = %err, + "wrapper proof generation failed" + ); + } + } + } + _ = cancellation_token.cancelled() => { + anyhow::bail!("Wrapper proof generate task cancelled"); + } + } + } + }) +} + +fn wrapper_output_path(output: &str, wrapper_task: &WrapperProof) -> String { + let path = Path::new(output); + let output_dir = path + .parent() + .filter(|parent| !parent.as_os_str().is_empty()) + .unwrap_or_else(|| Path::new(".")); + output_dir + .join(format!( + "{}-{}.bin", + wrapper_task.graph_id.as_simple(), + wrapper_task.operator_proof_id + )) + .to_string_lossy() + .to_string() +} + +fn read_utf8_sidecar(path: &str) -> anyhow::Result { + Ok(String::from_utf8(std::fs::read(path)?)?) +} + +#[cfg(test)] +mod tests { + use super::*; + use store::{OperatorProof, create_local_db}; + use uuid::Uuid; + + #[tokio::test] + async fn create_missing_wrapper_tasks_is_unique_and_only_consumes_proven_operators() { + let local_db = create_local_db("sqlite::memory:").await; + let instance_id = Uuid::parse_str("00112233445566778899aabbccddeeff").unwrap(); + let graph_id = Uuid::parse_str("ffeeddccbbaa99887766554433221100").unwrap(); + let new_graph_id = Uuid::parse_str("11111111111111111111111111111111").unwrap(); + let now = current_time_secs(); + + { + let mut storage_processor = local_db.acquire().await.unwrap(); + storage_processor + .create_operator_proof(&OperatorProof { + instance_id, + graph_id, + execution_layer_block_number: 9511055, + path_to_proof: Some("operator-proof.bin".to_string()), + public_value_hex: Some("operator-public".to_string()), + proof_state: ProofState::Proven.to_i64(), + created_at: now, + updated_at: now, + operator_committed_blockhash: + "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246" + .to_string(), + ..Default::default() + }) + .await + .unwrap(); + storage_processor + .create_operator_proof(&OperatorProof { + instance_id, + graph_id: new_graph_id, + execution_layer_block_number: 9511056, + path_to_proof: Some("operator-new.bin".to_string()), + proof_state: ProofState::New.to_i64(), + created_at: now, + updated_at: now, + operator_committed_blockhash: + "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246" + .to_string(), + ..Default::default() + }) + .await + .unwrap(); + } + + let genesis_txid = "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246"; + assert_eq!(create_missing_wrapper_tasks(&local_db, genesis_txid).await.unwrap(), 1); + assert_eq!(create_missing_wrapper_tasks(&local_db, genesis_txid).await.unwrap(), 0); + + let mut storage_processor = local_db.acquire().await.unwrap(); + let wrapper_proof = storage_processor + .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) + .await + .unwrap() + .expect("wrapper proof should exist"); + assert_eq!(wrapper_proof.operator_path_to_proof, "operator-proof.bin"); + assert_eq!(wrapper_proof.operator_public_value_hex.as_deref(), Some("operator-public")); + assert_eq!(wrapper_proof.proof_state, ProofState::New.to_i64()); + assert!( + storage_processor + .find_wrapper_proof_by_instance_and_graph(&instance_id, &new_graph_id) + .await + .unwrap() + .is_none() + ); + } +} From 824990b9f1591bfee97d989a3f8aa3530271facc Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:02:03 +0800 Subject: [PATCH 02/14] update validate_pubin_disprove (#405) Co-authored-by: ethan --- crates/bitvm-gc/src/verifier/api.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 5895c0aa..489d1915 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -9,8 +9,9 @@ use goat::{ scripts::{generate_opreturn_script, p2a_output}, transactions::{ assert::{pubin_disprove, validate_pubin}, - base::DUST_AMOUNT, + base::{DUST_AMOUNT, output_topology}, pre_signed::PreSignedTransaction, + watchtower_challenge::extract_operator_preimage_from_ack_txin, }, utils::num_blocks_per_network, }; @@ -179,8 +180,26 @@ pub fn validate_pubin_disprove( graph: &BitvmGcGraph, operator_commit_pubin_txin: &TxIn, operator_assert_txin: &TxIn, - ack_preimages: Vec>, + 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 From 821775bf67ee1406664f044866cbbae2ed4a3776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Gia=20Vi=E1=BB=87t=20Anh?= Date: Mon, 22 Jun 2026 17:04:25 +0700 Subject: [PATCH 03/14] Feat: Implement BitVM3 deferred binding. (#404) * build and broadcast operator commit pubin in handler. * change babe_adapter to adapt bitvm3-gc * use operator proof & adapt handler to bitvm3 partial binding. * adjust code after rebase * add tests for babe adapter * update validate_pubin_disprove (#405) Co-authored-by: ethan * update assert sent verifier * update handle_challenge_assert_sent_operator * update deps --------- Co-authored-by: KSlashh <48985735+KSlashh@users.noreply.github.com> Co-authored-by: ethan --- Cargo.lock | 9 +- Cargo.toml | 10 +- crates/bitvm-gc/src/babe_adapter.rs | 836 +++++++------------------- crates/bitvm-gc/tests/babe_adapter.rs | 237 +++++--- node/src/action.rs | 4 +- node/src/env.rs | 56 +- node/src/handle.rs | 430 +++++++++++-- node/src/utils.rs | 366 ++++++++++- 8 files changed, 1162 insertions(+), 786 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2f6a4613..66f2c452 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5576,7 +5576,7 @@ dependencies = [ [[package]] name = "garbled-snark-verifier" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12226,7 +12226,7 @@ dependencies = [ [[package]] name = "soldering-host" version = "1.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12236,6 +12236,7 @@ dependencies = [ "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", @@ -13911,7 +13912,7 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "aes", "ark-bn254", @@ -13930,9 +13931,11 @@ dependencies = [ "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-zkvm", ] diff --git a/Cargo.toml b/Cargo.toml index f8aab0ce..340b22a4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,9 +107,13 @@ p3-field = { git = "https://github.com/ProjectZKM/Plonky3" } #zkm-verifier = { path = "../Ziren/crates/verifier" } bitvm-lib = { package = "bitvm-gc", path = "crates/bitvm-gc" } -verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } -garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } -soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } +verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } + +#verifiable-circuit-babe = { path = "../bitvm2-gc/verifiable-circuit-babe"} +#garbled-snark-verifier = { path = "../bitvm2-gc/garbled-snark-verifier"} +#soldering-host = { path = "../bitvm2-gc/babe-programs/soldering/host" } store = { path = "crates/store" } util = { path = "crates/util" } client = { path = "crates/client" } diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index ccce0658..0f34a868 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -1,12 +1,12 @@ -use rayon::prelude::*; use std::collections::HashSet; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::PathBuf; use anyhow::{Result, bail}; -use ark_bn254::{Bn254, Fq, Fr}; +use ark_bn254::{Bn254, Fr}; +use ark_bn254::g1::G1Affine; use ark_groth16::VerifyingKey as Groth16VerifyingKey; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_serialize::CanonicalSerialize; use garbled_snark_verifier::bag::S; use goat::assert_scripts::{ INPUT_WIRE_NUM, OperatorAssertPublicKey, OperatorAssertSecretKey, WireHash, label_hash, @@ -16,25 +16,17 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use soldering_host::BabeBundle; pub use soldering_host::BabeBundleBuilder; -use verifiable_circuit_babe::babe::{ - LAMPORT_N, ProverSetupState, WeKnownPi1SetupCt as RealSetupCt, babe_verifier_presign, -}; -use verifiable_circuit_babe::cac::{ - CACSetupPackage as RealCACSetupPackage, FinalizedInstanceData as RealFinalizedInstanceData, - cac_finalize_indices, -}; -use verifiable_circuit_babe::gc::{ - SparseAdaptorEntry as RealSparseAdaptorEntry, SparseAdaptorRow as RealSparseAdaptorRow, - SparseAdaptorTable as RealSparseAdaptorTable, -}; -use verifiable_circuit_babe::instance::BABEInstance; -use verifiable_circuit_babe::instance::commit::CACInstanceCommit as RealCACInstanceCommit; +use verifiable_circuit_babe::babe::{WeKnownPi1SetupCt, GC_INPUT_WIRES, build_challenge_assert_witness_raw, deinterleave_dummy_positions, interleave_dummy_positions}; +pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; +use verifiable_circuit_babe::cac::cac_finalize_indices; +use verifiable_circuit_babe::gc::{SparseAdaptorTable, SGC_PART1_CONSTANT_SIZE}; +pub use verifiable_circuit_babe::instance::commit::CACInstanceCommit; use verifiable_circuit_babe::prover::BABEProver; -use verifiable_circuit_babe::soldering::{ - SolderingData as RealSolderingData, SolderingProof as RealSolderingProof, -}; +use verifiable_circuit_babe::soldering::{SolderedLabelsData, SolderingData as RealSolderingData, SolderingProof as RealSolderingProof}; +pub use verifiable_circuit_babe::transactions::TxAssertWitness; +pub use verifiable_circuit_babe::transactions::ChallengeAssertWitnessRaw; +use verifiable_circuit_babe::utils::pi1_xd_to_wots96_msg; use verifiable_circuit_babe::verifier::BABEVerifier; - use crate::types::BitvmGcCircuitData; /// Number of Wots96 digit signatures expected by the GOAT GC-V2 connector. @@ -47,48 +39,6 @@ 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 CACSetupPackage { - pub commits: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CACInstanceCommit { - pub epk: Vec<[[u8; 20]; 2]>, - pub wots_padding_epk: [[[u8; 20]; 2]; 4], - pub constant_commits: [[[u8; 32]; 2]; 2], - pub h_msg: [u8; 20], - pub h_ct_setup: [u8; 32], - pub com_adaptor: [u8; 32], - pub com_gc: [u8; 32], -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct FinalizedInstanceData { - pub index: usize, - pub final_msg_hash: [u8; 20], - pub wire_hashes: Vec, - pub gc_commitment: [u8; 32], - pub adaptor_commitment: [u8; 32], - pub ct_setup_commitment: [u8; 32], - pub real_data: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RealFinalizedPayload { - pub gc_ciphertexts: Vec>, - pub adaptor_table: SerializableSparseAdaptorTable, - pub ct_setup: SerializableSetupCt, - pub constant_labels: [[u8; 16]; 2], - pub wots_padding_zero_labels: [[u8; 16]; 4], -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CompactFinalizedInstanceData { - pub index: usize, - pub real_data: RealFinalizedPayload, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactSolderingData { pub finalized_indices: Vec, @@ -98,33 +48,10 @@ pub struct CompactSolderingData { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactSolderingProofPayload { pub opened: OpenedInstanceSeeds, - pub finalized: Vec, + pub finalized: Vec, pub soldering: CompactSolderingData, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSetupCt { - pub ct2_r_delta_g2: Vec, - pub ct3_masked_msg: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorTable { - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorEntry { - pub x: SerializableSparseAdaptorRow, - pub y: SerializableSparseAdaptorRow, - pub z: SerializableSparseAdaptorRow, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorRow { - pub cts: Vec<[u8; 32]>, - pub offset: Vec, -} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SolderingData { @@ -133,17 +60,10 @@ pub struct SolderingData { pub proof: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SolderedLabelsData { - pub base_commitment: Vec<([u8; 32], [u8; 32])>, - pub deltas: Vec>, - pub commitments: Vec>, -} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeVerifierPrivateState { pub instance_seeds: Vec, - pub temp_val: [u8; 32], pub statement_digest: [u8; 32], } @@ -162,79 +82,79 @@ pub struct BabeProverState { pub h_msgs: Vec<[u8; 20]>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BabeAssertWitness { - pub pi1: Vec, - #[serde(default)] - pub pubin_commitment: [u8; 32], - pub wots_sig: Vec<[u8; 21]>, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeChallengeAssertWitness { pub verifier_index: usize, - pub input_labels: Vec<[u8; 16]>, - pub wots_sig: Vec<[u8; 21]>, + pub witness: ChallengeAssertWitnessRaw, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeWronglyChallengedWitness { pub verifier_index: usize, - pub final_msgs: Vec>, + // just need to contain one valid msg + pub final_msg: Vec, } -impl CACInstanceCommit { - /// Builds deterministic placeholder setup commitments for tests and wiring. - pub fn sample(seed: u8) -> Self { - let epk = (0..LAMPORT_N) - .map(|wire| [hash20(&[seed, wire as u8, 0]), hash20(&[seed, wire as u8, 1])]) - .collect(); - Self { - epk, - wots_padding_epk: padding_wire_hashes(), - constant_commits: [ - [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])], - [hash32(&[seed, 0xf1, 0]), hash32(&[seed, 0xf1, 1])], - ], - h_msg: hash20(&[seed, 0xa0]), - h_ct_setup: hash32(&[seed, 0xa1]), - com_adaptor: hash32(&[seed, 0xa2]), - com_gc: hash32(&[seed, 0xa3]), - } - } -} -impl FinalizedInstanceData { - /// Builds deterministic placeholder finalized data for tests and graph wiring. - pub fn sample(index: usize) -> Self { - let seed = index as u8; - let wire_hashes = (0..INPUT_WIRE_NUM) - .map(|wire| WireHash { - true_label_hash: hash20(&[seed, wire as u8, 1]), - false_label_hash: hash20(&[seed, wire as u8, 0]), - }) - .collect(); - Self { - index, - final_msg_hash: hash20(&[seed, 0xb0]), - wire_hashes, - gc_commitment: hash32(&[seed, 0xb1]), - adaptor_commitment: hash32(&[seed, 0xb2]), - ct_setup_commitment: hash32(&[seed, 0xb3]), - real_data: None, +/// 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 = WeKnownPi1SetupCt { + ct2_r_delta_g2: vec![], + ct3_masked_msg: vec![], + }; -impl CompactFinalizedInstanceData { - pub fn try_from_finalized(finalized: &FinalizedInstanceData) -> Result { - let real_data = finalized.real_data.clone().ok_or_else(|| { - anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) - })?; - Ok(Self { index: finalized.index, real_data }) + 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(), } } + impl SolderingData { /// Builds deterministic placeholder soldering data for the selected finalized indices. pub fn sample(finalized_indices: Vec) -> Self { @@ -248,7 +168,7 @@ pub fn build_setup_package(n_cc: usize) -> Result { bail!("n_cc must be greater than zero"); } Ok(CACSetupPackage { - commits: (0..n_cc).map(|index| CACInstanceCommit::sample(index as u8)).collect(), + commits: (0..n_cc).map(|index| sample_cac_instance_commit(index as u8)).collect(), }) } @@ -256,20 +176,19 @@ pub fn build_setup_package(n_cc: usize) -> Result { pub fn build_real_setup_package( n_cc: usize, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + 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, public_inputs))) + 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 = from_real_package(&verifier.commit()); + let package = verifier.commit(); let private_state = BabeVerifierPrivateState { - instance_seeds: verifier.instances.iter().map(|instance| instance.seed).collect(), - temp_val: verifier.temp_val, - statement_digest: statement_digest(vk, public_inputs)?, + instance_seeds: verifier.get_seeds(), + statement_digest: statement_digest(vk, static_inputs)?, }; Ok((package, private_state)) @@ -282,15 +201,15 @@ pub fn open_real_setup_and_solder( package: &CACSetupPackage, finalized_indices: &[usize], vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result { ensure_real_gc_assets_configured()?; - if private_state.statement_digest != statement_digest(vk, public_inputs)? { + 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, vk, public_inputs)?; - if from_real_package(&verifier.commit()) != *package { + 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 @@ -298,11 +217,7 @@ pub fn open_real_setup_and_solder( .map_err(anyhow::Error::msg)?; Ok(( bundle.opened, - bundle - .finalized - .iter() - .map(|data| from_real_finalized(data, package)) - .collect::>>()?, + bundle.finalized, from_real_soldering(&bundle.soldering)?, )) } @@ -315,22 +230,15 @@ pub fn verify_real_setup( finalized: &[FinalizedInstanceData], soldering: &SolderingData, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_public_inputs: Fr, ) -> Result<()> { - let real_package = to_real_package(package); - let real_finalized = finalized - .iter() - .map(to_real_finalized) - .collect::>>()?; - let bundle = BabeBundle { opened: opened.to_vec(), - finalized: real_finalized, + finalized: finalized.to_vec(), soldering: to_real_soldering(soldering)?, - temp_hashlock: [0u8; 20], }; soldering_builder - .babe_prover_verify_setup(&real_package, &bundle, vk, public_inputs) + .babe_prover_verify_setup(&package, &bundle, vk, static_public_inputs) .map_err(anyhow::Error::msg) } @@ -342,10 +250,7 @@ pub fn compact_soldering_proof_payload( ) -> Result { Ok(CompactSolderingProofPayload { opened: opened.to_vec(), - finalized: finalized - .iter() - .map(CompactFinalizedInstanceData::try_from_finalized) - .collect::>>()?, + finalized: finalized.to_vec(), soldering: CompactSolderingData { finalized_indices: soldering.finalized_indices.clone(), proof: soldering.proof.clone(), @@ -355,14 +260,9 @@ pub fn compact_soldering_proof_payload( /// Reconstructs the full BABE setup data using the locally trusted setup package. pub fn expand_compact_soldering_proof_payload( - package: &CACSetupPackage, payload: CompactSolderingProofPayload, ) -> Result { - let finalized = payload - .finalized - .into_iter() - .map(|data| expand_compact_finalized_instance(package, data)) - .collect::>>()?; + let finalized = payload.finalized.clone(); Ok((payload.opened, finalized, expand_compact_soldering_data(payload.soldering)?)) } @@ -372,7 +272,7 @@ pub fn derive_finalized_indices(package: &CACSetupPackage, m_cc: usize) -> Resul if m_cc == 0 || m_cc > n_cc { bail!("invalid m_cc {m_cc} for n_cc {n_cc}"); } - Ok(cac_finalize_indices(&to_real_package(package), m_cc)) + Ok(cac_finalize_indices(n_cc, m_cc)) } /// Opens non-finalized placeholder instances and returns finalized data plus soldering data. @@ -392,7 +292,7 @@ pub fn open_and_solder( .map(|index| (index, deterministic_seed(index))) .collect::>(); let finalized = - finalized_indices.iter().map(|index| FinalizedInstanceData::sample(*index)).collect(); + finalized_indices.iter().map(|index| sample_finalized_instance_data(*index)).collect(); let soldering = SolderingData::sample(finalized_indices.to_vec()); Ok((opened, finalized, soldering)) } @@ -427,36 +327,44 @@ pub fn verify_setup( if data.index >= n_cc { bail!("finalized index {} out of range", data.index); } - if data.wire_hashes.len() != INPUT_WIRE_NUM { - bail!("finalized index {} has invalid wire hash count", data.index); - } + } Ok(()) } /// Extracts one graph slot owned by `verifier_pubkey` from finalized setup data. +/// +/// `epk` must have exactly `GC_INPUT_WIRES` (762) entries pub fn extract_gc_circuit_data( - finalized: &[FinalizedInstanceData], - soldering: &SolderingData, verifier_pubkey: bitcoin::PublicKey, + epk: &[[[u8; 20]; 2]], + h_msgs: &[[u8; 20]], ) -> Result { - if finalized.len() != BABE_M_CC { - bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); + if epk.len() != GC_INPUT_WIRES { + bail!("BABE epk has {} entries; expected {GC_INPUT_WIRES}", epk.len()); } - if soldering.finalized_indices != finalized.iter().map(|data| data.index).collect::>() { - bail!("soldering finalized indices mismatch"); - } - let data = &finalized[0]; - let wire_hashes: [WireHash; INPUT_WIRE_NUM] = - data.wire_hashes.clone().try_into().map_err(|wire_hashes: Vec| { - anyhow::anyhow!( - "BABE input label count {} is incompatible with GOAT verifier connector wire count {INPUT_WIRE_NUM}", - wire_hashes.len() - ) - })?; + let n = GC_INPUT_WIRES / 3; + let to_wire_hash = |pair: &[[u8; 20]; 2]| WireHash { + false_label_hash: pair[0], + true_label_hash: pair[1], + }; + let dummy_hash = label_hash(&vec![0u8; 16]); + let dummy = WireHash { false_label_hash: dummy_hash, true_label_hash: dummy_hash }; + + let pi1_x: Vec = epk[..n].iter().map(to_wire_hash).collect(); + let pi1_y: Vec = epk[n..2 * n].iter().map(to_wire_hash).collect(); + let x_d: Vec = epk[2 * n..].iter().map(to_wire_hash).collect(); + let wire_hashes_vec = interleave_dummy_positions(&pi1_x, &pi1_y, &x_d, dummy); + + let wire_hashes: [WireHash; INPUT_WIRE_NUM] = wire_hashes_vec.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: finalized.iter().map(|data| data.final_msg_hash).collect(), + final_msg_hashlocks: h_msgs.to_vec(), wire_hashes, }) } @@ -465,54 +373,43 @@ pub fn extract_gc_circuit_data( pub fn build_assert_witness( proof: &ark_groth16::Proof, assert_secret_key: &OperatorAssertSecretKey, -) -> Result { - build_assert_witness_with_pubin_commitment(proof, &[0u8; 32], assert_secret_key) -} - -pub fn build_assert_witness_with_pubin_commitment( - proof: &ark_groth16::Proof, - pubin_commitment: &[u8; 32], - assert_secret_key: &OperatorAssertSecretKey, -) -> Result { + dynamic_input: ark_bn254::Fr, +) -> Result { if assert_secret_key.is_empty() { bail!("operator WOTS secret key must not be empty"); } - let pi1 = proof.a; - let mut pi1_bytes = Vec::new(); - pi1.serialize_compressed(&mut pi1_bytes).expect("serialize pi1"); - let msg = pi1_to_wots96_msg(&pi1, pubin_commitment); + let msg = pi1_xd_to_wots96_msg(&proof.a, dynamic_input); let wots_sig = Wots96::sign(assert_secret_key, &msg); - Ok(BabeAssertWitness { - pi1: pi1_bytes, - pubin_commitment: *pubin_commitment, - wots_sig: wots_sig.to_vec(), - }) + Ok(TxAssertWitness { wots_sig: wots_sig.to_vec() }) } -pub fn assert_wots_message(assert_witness: &BabeAssertWitness) -> Result<[u8; 96]> { - let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) - .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; - Ok(pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment)) +pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> { + let recover = assert_witness.recover_pi1_xd_without_verify(); + if recover.is_none() { + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + } + let (pi1, x_d) = recover.unwrap(); + Ok(pi1_xd_to_wots96_msg(&pi1, x_d)) } /// Builds a placeholder verifier challenge witness from an assert witness. pub fn build_challenge_assert_witness( verifier_state: &BabeVerifierState, - assert_witness: &BabeAssertWitness, + 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"); } - if assert_witness.pi1.is_empty() || assert_witness.wots_sig.is_empty() { - bail!("invalid assert witness"); - } + let bytes = assert_wots_message(assert_witness)?; Ok(BabeChallengeAssertWitness { verifier_index, - input_labels: (0usize..INPUT_WIRE_NUM) - .map(|index| hash16_with_index(&assert_witness.pi1, index)) - .collect(), - wots_sig: assert_witness.wots_sig.clone(), + witness: ChallengeAssertWitnessRaw { + input_labels: (0usize..INPUT_WIRE_NUM) + .map(|index| hash16_with_index(&bytes, index)) + .collect(), + wots_sig: assert_witness.wots_sig.clone(), + } }) } @@ -523,110 +420,105 @@ pub fn build_real_challenge_assert_witness( package: &CACSetupPackage, finalized_indices: &[usize], vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, operator_wots_pubkey: &OperatorAssertPublicKey, - assert_witness: &BabeAssertWitness, + 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, vk, public_inputs)?; - if from_real_package(&verifier.commit()) != *package { + 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 pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) - .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; + + let recover = assert_witness.recover_pi1_xd_without_verify(); + if recover.is_none() { + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + } + let (pi1, x_d) = recover.unwrap(); + let expected_message = pi1_xd_to_wots96_msg(&pi1, x_d); let wots_sig = to_real_wots_sig(&assert_witness.wots_sig)?; let signed_message = Wots96::signature_to_message(&wots_sig); - let expected_message = pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment); if signed_message != expected_message { bail!("operator BABE assertion WOTS signature message does not match pi1/pubin"); } let base_idx = finalized_indices[0]; - let base_inst = verifier - .instances - .get(base_idx) - .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; - let mut input_labels = base_inst - .compute_pi1_labels_based_on_value(pi1) - .into_iter() - .skip(2) - .map(|label| label.0) - .collect::>(); - let commit = package - .commits - .get(base_idx) - .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; - input_labels.extend(pubin_input_labels(commit, &assert_witness.pubin_commitment)); - if input_labels.len() != INPUT_WIRE_NUM { - bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", input_labels.len()); + + let witness = build_challenge_assert_witness_raw( + &verifier, + assert_witness, + operator_wots_pubkey, + base_idx, + ); + if witness.is_none() { + bail!("cannot generate challenge assert witness"); } - let _ = operator_wots_pubkey; - Ok(BabeChallengeAssertWitness { verifier_index, input_labels, wots_sig: wots_sig.to_vec() }) + let witness = witness.unwrap(); + + if witness.input_labels.len() != INPUT_WIRE_NUM { + bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", witness.input_labels.len()); + } + Ok(BabeChallengeAssertWitness { + verifier_index, + witness, + }) } -/// Builds a wrongly-challenged witness from every recovered finalized-message preimage. +/// 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_msgs: Vec>, + final_msg: Vec, ) -> Result { build_wrongly_challenged_witness_from_preimages( &prover_state.h_msgs, challenge_witness, - final_msgs, + final_msg, ) } -/// Evaluates the native BABE garbled circuit and returns all finalized hashlock preimages. +/// 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 real_state = ProverSetupState { - wots_sk_p: vec![], - finalized: prover_state - .finalized - .iter() - .map(to_real_finalized) - .collect::>>()?, - soldering: to_real_soldering(&prover_state.soldering)?, - h_msgs: prover_state.h_msgs.clone(), - presigs_v: babe_verifier_presign(), - }; - recover_all_finalized_messages(prover_state, challenge_witness, proof, &real_state) + 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_msgs: Vec>, + final_msg: Vec, ) -> Result { if h_msgs.len() != BABE_M_CC { bail!("wrongly challenged setup must contain exactly {BABE_M_CC} finalized hashlocks"); } - if final_msgs.len() != h_msgs.len() { - bail!("wrongly challenged witness must provide every finalized preimage"); - } - for (position, (final_msg, expected_hash)) in final_msgs.iter().zip(h_msgs).enumerate() { - if label_hash(final_msg) != *expected_hash { - bail!("message at finalized position {position} is not a valid preimage"); - } + + if !h_msgs.contains(&label_hash(&final_msg)) { + bail!("message is not a valid preimage"); } + Ok(BabeWronglyChallengedWitness { verifier_index: challenge_witness.verifier_index, - final_msgs, + final_msg, }) } fn ensure_real_gc_assets_configured() -> Result<()> { - for name in ["GC_GATES_PATH", "GC_INDICES_PATH"] { + 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 BABE setup"))?, + .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()); @@ -635,30 +527,27 @@ fn ensure_real_gc_assets_configured() -> Result<()> { Ok(()) } -fn statement_digest(vk: &Groth16VerifyingKey, public_inputs: &[Fr]) -> Result<[u8; 32]> { +fn statement_digest(vk: &Groth16VerifyingKey, static_inputs: Fr) -> Result<[u8; 32]> { let mut bytes = Vec::new(); vk.serialize_compressed(&mut bytes)?; - for public_input in public_inputs { - public_input.serialize_compressed(&mut bytes)?; - } + static_inputs.serialize_compressed(&mut bytes)?; Ok(hash32(&bytes)) } fn restore_real_verifier( state: &BabeVerifierPrivateState, + package: &CACSetupPackage, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result { - let instances = state - .instance_seeds - .par_iter() - .map(|seed| { - let mut instance = BABEInstance::new_from_seed(*seed); - instance.enc_setup(vk, public_inputs).map_err(anyhow::Error::msg)?; - Ok(instance) - }) - .collect::>>()?; - Ok(BABEVerifier { instances, temp_val: state.temp_val }) + let verifier = BABEVerifier::from_state( + &state.instance_seeds, package, vk, static_inputs + ); + if verifier.is_none() { + Err(anyhow::anyhow!("Cannot restore real verifier")) + } else { + Ok(verifier.unwrap()) + } } fn validate_finalized_indices( @@ -678,154 +567,6 @@ fn validate_finalized_indices( Ok(()) } -fn from_real_package(package: &RealCACSetupPackage) -> CACSetupPackage { - CACSetupPackage { commits: package.commits.iter().map(from_real_commit).collect() } -} - -fn from_real_commit(commit: &RealCACInstanceCommit) -> CACInstanceCommit { - CACInstanceCommit { - epk: commit.epk.clone(), - wots_padding_epk: padding_wire_hashes(), - constant_commits: commit.constant_commits, - h_msg: commit.h_msg, - h_ct_setup: commit.h_ct_setup, - com_adaptor: commit.com_adaptor, - com_gc: commit.com_gc, - } -} - -fn to_real_package(package: &CACSetupPackage) -> RealCACSetupPackage { - RealCACSetupPackage { - commits: package - .commits - .iter() - .map(|commit| RealCACInstanceCommit { - epk: commit.epk.clone(), - constant_commits: commit.constant_commits, - h_msg: commit.h_msg, - h_ct_setup: commit.h_ct_setup, - com_adaptor: commit.com_adaptor, - com_gc: commit.com_gc, - }) - .collect(), - } -} - -fn from_real_finalized( - finalized: &RealFinalizedInstanceData, - package: &CACSetupPackage, -) -> Result { - let real_data = RealFinalizedPayload { - gc_ciphertexts: finalized - .gc_ciphertexts - .iter() - .map(|ciphertext| ciphertext.map(|label| label.0)) - .collect(), - adaptor_table: from_real_adaptor_table(&finalized.adaptor_table), - ct_setup: SerializableSetupCt { - ct2_r_delta_g2: finalized.ct_setup.ct2_r_delta_g2.clone(), - ct3_masked_msg: finalized.ct_setup.ct3_masked_msg.clone(), - }, - constant_labels: [finalized.constant_labels[0].0, finalized.constant_labels[1].0], - wots_padding_zero_labels: [[0u8; 16]; 4], - }; - expand_compact_finalized_instance( - package, - CompactFinalizedInstanceData { index: finalized.index, real_data }, - ) -} - -fn expand_compact_finalized_instance( - package: &CACSetupPackage, - finalized: CompactFinalizedInstanceData, -) -> Result { - let commit = package - .commits - .get(finalized.index) - .ok_or_else(|| anyhow::anyhow!("finalized index {} out of range", finalized.index))?; - if commit.epk.len() != LAMPORT_N { - bail!( - "finalized index {} has {} BABE input commitments; expected {LAMPORT_N}", - finalized.index, - commit.epk.len() - ); - } - let mut wire_hashes = Vec::with_capacity(INPUT_WIRE_NUM); - wire_hashes.extend(commit.epk[..254].iter().map(to_wire_hash)); - wire_hashes.extend(commit.wots_padding_epk[..2].iter().map(to_wire_hash)); - wire_hashes.extend(commit.epk[254..].iter().map(to_wire_hash)); - wire_hashes.extend(commit.wots_padding_epk[2..].iter().map(to_wire_hash)); - wire_hashes.extend(pubin_wire_hashes(commit)); - Ok(FinalizedInstanceData { - index: finalized.index, - final_msg_hash: commit.h_msg, - wire_hashes, - gc_commitment: commit.com_gc, - adaptor_commitment: commit.com_adaptor, - ct_setup_commitment: commit.h_ct_setup, - real_data: Some(finalized.real_data), - }) -} - -fn to_real_finalized(finalized: &FinalizedInstanceData) -> Result { - let payload = finalized.real_data.as_ref().ok_or_else(|| { - anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) - })?; - Ok(RealFinalizedInstanceData { - index: finalized.index, - gc_ciphertexts: payload.gc_ciphertexts.iter().map(|value| value.map(S)).collect(), - adaptor_table: to_real_adaptor_table(&payload.adaptor_table)?, - ct_setup: RealSetupCt { - ct2_r_delta_g2: payload.ct_setup.ct2_r_delta_g2.clone(), - ct3_masked_msg: payload.ct_setup.ct3_masked_msg.clone(), - }, - constant_labels: [S(payload.constant_labels[0]), S(payload.constant_labels[1])], - }) -} - -fn from_real_adaptor_table(table: &RealSparseAdaptorTable) -> SerializableSparseAdaptorTable { - SerializableSparseAdaptorTable { - entries: table - .entries - .iter() - .map(|entry| SerializableSparseAdaptorEntry { - x: from_real_adaptor_row(&entry.x), - y: from_real_adaptor_row(&entry.y), - z: from_real_adaptor_row(&entry.z), - }) - .collect(), - } -} - -fn from_real_adaptor_row(row: &RealSparseAdaptorRow) -> SerializableSparseAdaptorRow { - let mut offset = Vec::new(); - row.offset.serialize_compressed(&mut offset).expect("serialize adaptor offset"); - SerializableSparseAdaptorRow { cts: row.cts.clone(), offset } -} - -fn to_real_adaptor_table(table: &SerializableSparseAdaptorTable) -> Result { - Ok(RealSparseAdaptorTable { - entries: table - .entries - .iter() - .map(|entry| { - Ok(RealSparseAdaptorEntry { - x: to_real_adaptor_row(&entry.x)?, - y: to_real_adaptor_row(&entry.y)?, - z: to_real_adaptor_row(&entry.z)?, - }) - }) - .collect::>>()?, - }) -} - -fn to_real_adaptor_row(row: &SerializableSparseAdaptorRow) -> Result { - Ok(RealSparseAdaptorRow { - cts: row.cts.clone(), - offset: Fq::deserialize_compressed(row.offset.as_slice())?, - }) -} - fn from_real_soldering(soldering: &RealSolderingData) -> Result { let output = soldering.soldering_proof.output().map_err(anyhow::Error::msg)?; Ok(SolderingData { @@ -866,131 +607,49 @@ fn to_real_soldering(soldering: &SolderingData) -> Result { }) } -fn recover_all_finalized_messages( +fn recover_a_valid_finalized_messages( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, - proof: &ark_groth16::Proof, - real_state: &ProverSetupState, + prover: &mut BABEProver, ) -> Result { - if real_state.finalized.len() != prover_state.h_msgs.len() { + if prover_state.finalized.len() != prover_state.h_msgs.len() { bail!("BABE prover state finalized data and hash count differ"); } - if real_state.finalized.is_empty() { + if prover_state.finalized.is_empty() { bail!("BABE prover state has no finalized instances"); } - let base_input_labels = pi1_labels_from_challenge(&challenge_witness.input_labels)?; + let base_input_labels: Vec = challenge_witness.witness.input_labels.iter().map(|&b| S(b)).collect(); + // Strip the 6 dummy labels before passing to the GC (which has GC_INPUT_WIRES real wires). + let (pi1_x_labels, pi1_y_labels, x_d_labels) = deinterleave_dummy_positions(&base_input_labels); + let pi1_labels: Vec = pi1_x_labels.into_iter().chain(pi1_y_labels).collect(); + let real_soldering = to_real_soldering(&prover_state.soldering)?; + let soldered_output = &prover_state.soldering.soldered_output; - if soldered_output.base_commitment.len() != base_input_labels.len() { + if soldered_output.base_commitment.len() != GC_INPUT_WIRES { bail!( - "soldering base commitment count {} does not match BABE input label count {}", - soldered_output.base_commitment.len(), - base_input_labels.len() + "soldering base commitment count {} does not match expected GC input wire count {GC_INPUT_WIRES}", + soldered_output.base_commitment.len() ); } - let prover = BABEProver::new(proof.clone()); - let final_msgs = real_state - .finalized - .iter() - .zip(&prover_state.h_msgs) - .enumerate() - .map(|(position, (finalized, expected_hash))| { - let input_labels = - soldered_input_labels(&base_input_labels, soldered_output, position)?; - recover_finalized_message( - &prover, - proof, - finalized, - &input_labels, - *expected_hash, - position, - ) - }) - .collect::>>()?; - - Ok(BabeWronglyChallengedWitness { - verifier_index: challenge_witness.verifier_index, - final_msgs, - }) -} + let found = prover.check_compute_msg( + &prover_state.finalized, + &pi1_labels, + &x_d_labels, + &real_soldering, + &prover_state.h_msgs, + ); -fn pi1_labels_from_challenge(input_labels: &[[u8; 16]]) -> Result> { - if input_labels.len() == LAMPORT_N { - return Ok(input_labels.iter().copied().map(S).collect()); - } - if input_labels.len() != INPUT_WIRE_NUM { - bail!( - "challenge witness has {} input labels; expected {LAMPORT_N} or {INPUT_WIRE_NUM}", - input_labels.len() - ); - } - Ok(input_labels[..254].iter().chain(&input_labels[256..510]).copied().map(S).collect()) -} -fn soldered_input_labels( - base_input_labels: &[S], - soldered_output: &SolderedLabelsData, - finalized_position: usize, -) -> Result> { - if finalized_position == 0 { - return Ok(base_input_labels.to_vec()); - } - let deltas = soldered_output.deltas.get(finalized_position - 1).ok_or_else(|| { - anyhow::anyhow!("missing soldering deltas for finalized position {finalized_position}") - })?; - if deltas.len() != base_input_labels.len() { - bail!( - "soldering delta count {} does not match BABE input label count {}", - deltas.len(), - base_input_labels.len() - ); + if !found { + bail!("Cannot find any valid msg"); } - Ok(base_input_labels - .iter() - .enumerate() - .map(|(wire, &base_label)| { - let (delta_false, delta_true) = deltas[wire]; - if hash32(&base_label.0) == soldered_output.base_commitment[wire].0 { - base_label ^ S(delta_false) - } else { - base_label ^ S(delta_true) - } - }) - .collect()) -} - -fn recover_finalized_message( - prover: &BABEProver, - proof: &ark_groth16::Proof, - finalized: &RealFinalizedInstanceData, - input_labels: &[S], - expected_hash: [u8; 20], - finalized_position: usize, -) -> Result> { - let mut full_labels = Vec::with_capacity(2 + input_labels.len()); - full_labels.push(finalized.constant_labels[0]); - full_labels.push(finalized.constant_labels[1]); - full_labels.extend_from_slice(input_labels); - - let (mut circuit, gc_output_indices) = verifiable_circuit_babe::gc::read_fresh_circuit(); - let ct_prove = prover.compute_ct_prove( - &mut circuit, - &gc_output_indices, - &full_labels, - &finalized.gc_ciphertexts, - &finalized.adaptor_table, - ); - let msg = BABEProver::compute_msg(proof, &ct_prove, &finalized.ct_setup) - .map_err(anyhow::Error::msg)?; - let final_msg = msg.to_vec(); - if label_hash(&final_msg) != expected_hash { - bail!( - "recovered message at finalized position {finalized_position} does not match hashlock" - ); - } - Ok(final_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 to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature> { @@ -1002,65 +661,10 @@ fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature }) } -fn padding_wire_hashes() -> [[[u8; 20]; 2]; 4] { - let false_hash = label_hash(&vec![0u8; 16]); - let true_hash = label_hash(&vec![1u8; 16]); - [[false_hash, true_hash]; 4] -} - 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 to_wire_hash(pair: &[[u8; 20]; 2]) -> WireHash { - WireHash { false_label_hash: pair[0], true_label_hash: pair[1] } -} - -fn pi1_to_wots96_msg(pi1: &ark_bn254::G1Affine, pubin_commitment: &[u8; 32]) -> [u8; 96] { - let mut msg = [0u8; 96]; - let mut tmp = Vec::new(); - - pi1.x.serialize_uncompressed(&mut tmp).expect("serialize pi1.x"); - msg[..32].copy_from_slice(&tmp); - - tmp.clear(); - pi1.y.serialize_uncompressed(&mut tmp).expect("serialize pi1.y"); - msg[32..64].copy_from_slice(&tmp); - - msg[64..96].copy_from_slice(pubin_commitment); - msg -} - -fn pubin_wire_hashes(commit: &CACInstanceCommit) -> Vec { - (0..256) - .map(|index| WireHash { - false_label_hash: label_hash(&pubin_label(commit, index, false).to_vec()), - true_label_hash: label_hash(&pubin_label(commit, index, true).to_vec()), - }) - .collect() -} - -fn pubin_input_labels(commit: &CACInstanceCommit, pubin_commitment: &[u8; 32]) -> Vec<[u8; 16]> { - (0..256) - .map(|index| { - let byte = pubin_commitment[index / 8]; - let bit = ((byte >> (index % 8)) & 1) == 1; - pubin_label(commit, index, bit) - }) - .collect() -} - -fn pubin_label(commit: &CACInstanceCommit, index: usize, bit: bool) -> [u8; 16] { - let mut bytes = Vec::with_capacity(32 * 4 + std::mem::size_of::() + 1); - bytes.extend_from_slice(&commit.h_ct_setup); - bytes.extend_from_slice(&commit.com_adaptor); - bytes.extend_from_slice(&commit.com_gc); - bytes.extend_from_slice(&commit.h_msg); - bytes.extend_from_slice(&(index as u64).to_le_bytes()); - bytes.push(u8::from(bit)); - hash16(&bytes) -} - fn hash20(data: &[u8]) -> [u8; 20] { let hash = hash32(data); hash[0..20].try_into().expect("20 bytes") diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index 19af1c2a..c8bf0798 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -4,14 +4,12 @@ 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, - CACInstanceCommit, CACSetupPackage, 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, verify_real_setup, - verify_setup, + 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, verify_real_setup, verify_setup, + sample_cac_instance_commit, sample_finalized_instance_data }; -use bitvm_gc::operator::generate_wots_key; use rand::SeedableRng; use rand_chacha::ChaCha12Rng; use std::collections::HashSet; @@ -26,7 +24,7 @@ fn verifier_pubkey() -> bitcoin::PublicKey { } #[test] -#[ignore = "requires GC_GATES_PATH and GC_INDICES_PATH runtime assets"] +#[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); @@ -36,10 +34,11 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &mut rng, ) .expect("groth16 setup"); - let public_inputs = vec![a * b]; + let static_public_inputs = a * b; + let _dynamic_public_inputs = a * a; let (package, private_state) = - build_real_setup_package(BABE_M_CC, &vk, &public_inputs).expect("real setup"); + 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"); @@ -51,7 +50,7 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &package, &finalized_indices, &vk, - &public_inputs, + static_public_inputs, ) .expect("open real setup"); @@ -64,17 +63,19 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &finalized, &soldering, &vk, - &public_inputs, + static_public_inputs, ) .expect("verify soldering proof"); - extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + 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 508-wire graph data"); } #[test] fn babe_setup_payload_round_trips_and_derives_gc_data() { let package = CACSetupPackage { - commits: (0..BABE_M_CC).map(|index| CACInstanceCommit::sample(index as u8)).collect(), + 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"); @@ -87,10 +88,11 @@ fn babe_setup_payload_round_trips_and_derives_gc_data() { let finalized = finalized_indices .iter() - .map(|index| FinalizedInstanceData::sample(*index)) + .map(|index| sample_finalized_instance_data(*index)) .collect::>(); - let soldering = SolderingData::sample(finalized_indices); - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + 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()); @@ -99,43 +101,45 @@ fn babe_setup_payload_round_trips_and_derives_gc_data() { #[test] fn protocol_finalized_instances_contribute_one_base_wire_slot() { - let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); - let soldering = SolderingData::sample((0..BABE_M_CC).collect()); + 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(&finalized, &soldering, verifier_pubkey()) + let gc_data = extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) .expect("one verifier graph slot"); - assert_eq!( - gc_data.final_msg_hashlocks, - finalized.iter().map(|data| data.final_msg_hash).collect::>() - ); - assert!(gc_data.wire_hashes.as_slice() == finalized[0].wire_hashes.as_slice()); + // M finalized instances each contribute one hashlock … + assert_eq!(gc_data.final_msg_hashlocks, h_msgs); + let n = GC_INPUT_WIRES / 3; // 254 real wires per group + // pi1_x: positions 0..n + 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 + 2].false_label_hash, epk[n][0]); + assert_eq!(gc_data.wire_hashes[2 * (n + 2)].false_label_hash, epk[2 * n][0]); + assert_eq!(gc_data.wire_hashes[2 * (n + 2) + n - 1].false_label_hash, epk[3 * n - 1][0]); } #[test] -fn gc_slot_rejects_invalid_finalized_counts_and_soldering_order() { - for count in [1, 3, 5, 8] { - let finalized = (0..count).map(FinalizedInstanceData::sample).collect::>(); - let soldering = SolderingData::sample((0..count).collect()); - let error = match extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) { - Ok(_) => panic!("invalid finalized count"), - Err(error) => error, - }; - assert!(error.to_string().contains(&format!("exactly {BABE_M_CC} finalized"))); +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, INPUT_WIRE_NUM] { + 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}" + ), + } } - - let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); - let mut indices = (0..BABE_M_CC).collect::>(); - indices.swap(0, 1); - let error = match extract_gc_circuit_data( - &finalized, - &SolderingData::sample(indices), - verifier_pubkey(), - ) { - Ok(_) => panic!("mismatched soldering order"), - Err(error) => error, - }; - assert!(error.to_string().contains("soldering finalized indices mismatch")); } #[test] @@ -146,8 +150,6 @@ fn finalized_indices_reject_invalid_counts_and_cover_full_cut() { assert!(derive_finalized_indices(&package, 5).is_err()); let first = derive_finalized_indices(&package, 4).expect("derive full cut"); - let second = derive_finalized_indices(&package, 4).expect("derive full cut again"); - assert_eq!(first, second); 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])); @@ -184,12 +186,24 @@ fn verify_setup_accepts_valid_opening_and_rejects_invalid_shapes() { #[test] fn witness_builders_validate_inputs_and_indices() { - let (assert_secret_key, _) = generate_wots_key("graph-scoped-key"); - let proof = ark_groth16::Proof::::default(); - let assert_witness = build_assert_witness(&proof, &assert_secret_key).expect("assert witness"); + 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::default(), + c: ark_bn254::G1Affine::default(), + }; + 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.pi1.is_empty()); - assert!(build_assert_witness(&proof, &Vec::new()).is_err()); + assert!(assert_witness.recover_pi1_xd_without_verify().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"), @@ -199,61 +213,63 @@ fn witness_builders_validate_inputs_and_indices() { 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.input_labels.len(), INPUT_WIRE_NUM); + assert_eq!(challenge_witness.witness.input_labels.len(), INPUT_WIRE_NUM); - let final_msgs = (0..BABE_M_CC) - .map(|index| format!("finalized-preimage-{index}").into_bytes()) - .collect::>(); - let h_msgs = final_msgs.iter().map(label_hash).collect::>(); + 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(FinalizedInstanceData::sample).collect(), + 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_msgs.clone()) + 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_msgs, final_msgs); + assert_eq!(wrongly_challenged.final_msg, final_msg); assert!( build_wrongly_challenged_witness( &prover_state, &challenge_witness, - vec![b"missing-preimage".to_vec(); BABE_M_CC], + b"missing-preimage".to_vec(), ) .is_err() ); } #[test] -fn wrongly_challenged_witness_requires_all_finalized_preimages() { - let final_msgs = (0..BABE_M_CC) - .map(|index| format!("finalized-preimage-{index}").into_bytes()) - .collect::>(); - let h_msgs = final_msgs.iter().map(label_hash).collect::>(); - let challenge_witness = - BabeChallengeAssertWitness { verifier_index: 0, input_labels: vec![], wots_sig: vec![] }; +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, - final_msgs.clone(), + valid_msg.clone(), ) .expect("wrongly challenged witness"); assert_eq!(from_preimages.verifier_index, 0); - assert_eq!(from_preimages.final_msgs, final_msgs); + 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(FinalizedInstanceData::sample).collect(), + 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_msgs.clone(), + from_preimages.final_msg.clone(), ) .expect("delegated wrongly challenged witness"); assert_eq!(delegated, from_preimages); @@ -262,8 +278,79 @@ fn wrongly_challenged_witness_requires_all_finalized_preimages() { build_wrongly_challenged_witness_from_preimages( &prover_state.h_msgs, &challenge_witness, - vec![from_preimages.final_msgs[0].clone(); BABE_M_CC], + 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![] }; + 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::default(), + c: ark_bn254::G1Affine::default(), + }; + + let assert_witness = build_assert_witness(&proof, &sk, dynamic_input).expect("assert witness"); + + let (recovered_pi1, recovered_xd) = assert_witness + .recover_pi1_xd_without_verify() + .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); +} diff --git a/node/src/action.rs b/node/src/action.rs index 08e22cbf..16b87a3d 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -12,7 +12,7 @@ use anyhow::{Context, Result, anyhow}; use bitcoin::{PublicKey, Txid}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{ - BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, + BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, TxAssertWitness, CACSetupPackage, }; use bitvm_lib::committee::*; @@ -245,7 +245,7 @@ pub struct AssertSent { pub instance_id: Uuid, pub graph_id: Uuid, pub assert_txid: Txid, - pub assert_witness: Option, + pub assert_witness: Option, } #[derive(Serialize, Deserialize, Clone)] pub struct ChallengeAssertSent { diff --git a/node/src/env.rs b/node/src/env.rs index b923552d..d79a46af 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -79,12 +79,19 @@ pub const ENV_GOAT_NETWORK: &str = "GOAT_NETWORK"; pub const ENV_WATCHTOWER_PROOF_WAIT_SECS: &str = "WATCHTOWER_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_PROOF_WAIT_SECS: &str = "OPERATOR_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_VK_HASH: &str = "OPERATOR_VK_HASH"; +pub const ENV_OPERATOR_ZKM_VERSION: &str = "OPERATOR_ZKM_VERSION"; pub const ENV_OPERATOR_WRAPPER_VK_HASH: &str = "OPERATOR_WRAPPER_VK_HASH"; pub const ENV_OPERATOR_WRAPPER_ZKM_VERSION: &str = "OPERATOR_WRAPPER_ZKM_VERSION"; pub const DEFAULT_WATCHTOWER_PROOF_WAIT_SECS: usize = 60; pub const DEFAULT_OPERATOR_PROOF_WAIT_SECS: usize = 60; -pub const ENV_GC_GATES_PATH: &str = "GC_GATES_PATH"; -pub const ENV_GC_INDICES_PATH: &str = "GC_INDICES_PATH"; +pub const ENV_FGC_GATES_PATH: &str = "FGC_GATES_PATH"; +pub const ENV_FGC_OUT_INDICES_PATH: &str = "FGC_OUT_INDICES_PATH"; +pub const ENV_SGC_GATES_PATH: &str = "SGC_GATES_PATH"; +pub const ENV_SGC_OUT_INDICES_PATH: &str = "SGC_OUT_INDICES_PATH"; +pub const ENV_FGC_COMPACT_GATES_PATH: &str = "FGC_COMPACT_GATES_PATH"; +pub const ENV_FGC_COMPACT_OUT_INDICES_PATH: &str = "FGC_COMPACT_OUT_INDICES_PATH"; +pub const ENV_SGC_COMPACT_GATES_PATH: &str = "SGC_COMPACT_GATES_PATH"; +pub const ENV_SGC_COMPACT_OUT_INDICES_PATH: &str = "SGC_COMPACT_OUT_INDICES_PATH"; pub const ENV_BABE_SETUP_STATE_DIR: &str = "BABE_SETUP_STATE_DIR"; pub const ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH: &str = "SOLDERING_PROOF_PAYLOAD_STORE_PATH"; @@ -601,6 +608,11 @@ pub fn get_operator_vk_hash() -> anyhow::Result<[u8; 32]> { hex_parse::<32>(&value).map_err(|err| anyhow::anyhow!("invalid {ENV_OPERATOR_VK_HASH}: {err}")) } +pub fn get_operator_zkm_version() -> anyhow::Result { + std::env::var(ENV_OPERATOR_ZKM_VERSION) + .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_ZKM_VERSION} needs to be set")) +} + pub fn get_operator_wrapper_vk_hash() -> anyhow::Result { std::env::var(ENV_OPERATOR_WRAPPER_VK_HASH) .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_VK_HASH} needs to be set")) @@ -611,23 +623,37 @@ pub fn get_operator_wrapper_zkm_version() -> anyhow::Result { .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_ZKM_VERSION} needs to be set")) } -/// Returns the configured GC asset paths after checking that they are readable files. -/// TODO: maybe multi files -pub fn get_babe_gc_asset_paths() -> anyhow::Result<(PathBuf, PathBuf)> { - let gates_path = PathBuf::from( - std::env::var(ENV_GC_GATES_PATH) - .map_err(|_| anyhow::anyhow!("{ENV_GC_GATES_PATH} is missing"))?, - ); - let indices_path = PathBuf::from( - std::env::var(ENV_GC_INDICES_PATH) - .map_err(|_| anyhow::anyhow!("{ENV_GC_INDICES_PATH} is missing"))?, - ); - for (name, path) in [(ENV_GC_GATES_PATH, &gates_path), (ENV_GC_INDICES_PATH, &indices_path)] { +pub struct BabeGcAssetPaths { + pub fgc_gates: PathBuf, + pub fgc_out_indices: PathBuf, + pub sgc_gates: PathBuf, + pub sgc_out_indices: PathBuf, + pub fgc_compact_gates: PathBuf, + pub fgc_compact_out_indices: PathBuf, + pub sgc_compact_gates: PathBuf, + pub sgc_compact_out_indices: PathBuf, +} + +pub fn get_babe_gc_asset_paths() -> anyhow::Result { + fn read_path(name: &str) -> anyhow::Result { + let path = PathBuf::from( + std::env::var(name).map_err(|_| anyhow::anyhow!("{name} is missing"))?, + ); if !path.is_file() { anyhow::bail!("{name} does not point to a readable file: {}", path.display()); } + Ok(path) } - Ok((gates_path, indices_path)) + Ok(BabeGcAssetPaths { + fgc_gates: read_path(ENV_FGC_GATES_PATH)?, + fgc_out_indices: read_path(ENV_FGC_OUT_INDICES_PATH)?, + sgc_gates: read_path(ENV_SGC_GATES_PATH)?, + sgc_out_indices: read_path(ENV_SGC_OUT_INDICES_PATH)?, + fgc_compact_gates: read_path(ENV_FGC_COMPACT_GATES_PATH)?, + fgc_compact_out_indices: read_path(ENV_FGC_COMPACT_OUT_INDICES_PATH)?, + sgc_compact_gates: read_path(ENV_SGC_COMPACT_GATES_PATH)?, + sgc_compact_out_indices: read_path(ENV_SGC_COMPACT_OUT_INDICES_PATH)?, + }) } pub fn get_instance_maintenance_batch_size() -> u32 { diff --git a/node/src/handle.rs b/node/src/handle.rs index eb2f385c..e67785c8 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -15,13 +15,7 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; -use bitvm_lib::babe_adapter::{ - BABE_M_CC, BABE_N_CC, BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, - BabeProverState, CACSetupPackage, CompactSolderingProofPayload, assert_wots_message, - build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, - derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, - open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, -}; +use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, TxAssertWitness, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage, WOTS_SIG_COUNT}; use bitvm_lib::committee::*; use bitvm_lib::keys::*; use bitvm_lib::operator::*; @@ -34,10 +28,11 @@ use goat::connectors::connector_z::ConnectorZ; use goat::transactions::base::output_topology; use goat::transactions::pre_signed::PreSignedTransaction; use goat::transactions::pre_signed_musig2::verify_public_nonce; +use goat::wots::{Wots, Wots96}; use libp2p::gossipsub::MessageId; use libp2p::{PeerId, Swarm}; use std::sync::Arc; -use store::GraphStatus; +use store::{GraphStatus, SerializableTxid}; use store::localdb::LocalDB; use uuid::Uuid; @@ -1208,9 +1203,9 @@ async fn handle_init_graph_verifier( } else { get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE setup")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let (setup_package, private_state) = tokio::task::spawn_blocking(move || { - build_real_setup_package(BABE_N_CC, &vk, &public_inputs) + build_real_setup_package(BABE_N_CC, &vk, static_input) }) .await .context("real BABE setup task failed")??; @@ -1407,7 +1402,7 @@ async fn handle_cut_circuits_verifier( get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE opening")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let private_state = verifier_state.private_state.clone(); let selected_indices = selected_circuit_indexes.clone(); let package_for_opening = setup_package.clone(); @@ -1423,7 +1418,7 @@ async fn handle_cut_circuits_verifier( &package_for_opening, &selected_indices, &vk, - &public_inputs, + static_input, ) }) .await @@ -1659,11 +1654,11 @@ async fn handle_compact_soldering_proof_operator( } let setup_package = candidate.setup_package.clone(); let (opened, finalized, soldering) = - expand_compact_soldering_proof_payload(&setup_package, payload) + expand_compact_soldering_proof_payload(payload) .context("expand compact soldering proof payload")?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE validation")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let package_for_validation = setup_package.clone(); let opened_for_validation = opened.clone(); let finalized_for_validation = finalized.clone(); @@ -1682,13 +1677,18 @@ async fn handle_compact_soldering_proof_operator( &finalized_for_validation, &soldering_for_validation, &vk, - &public_inputs, + static_input, ) }) .await .context("real BABE setup verification task failed")??; - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey)?; + if finalized.len() != BABE_M_CC { + bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); + } + let epk = &setup_package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| setup_package.commits[f.index].h_msg).collect(); + let gc_data = extract_gc_circuit_data(verifier_pubkey, epk, &h_msgs)?; let prover_state = BabeProverState { package: setup_package.clone(), finalized, @@ -3560,17 +3560,52 @@ async fn handle_operator_commit_pubin_ready_operator( content: &GOATMessageContent, ) -> Result<()> { let message = make_message(ctx, content); - let Some((_graph, graph_status, _graph_sub_status)) = + let Some((graph, _graph_status, _graph_sub_status)) = refresh_graph_status(ctx, instance_id, graph_id, Some(&message), GraphStatus::Challenge) .await? else { return Ok(()); }; - // TODO(gc-v2): build operator-commit-pubin after pubin commitment data is wired. - // It is not pre-signed, so add a funding input for fees and broadcast it normally. - tracing::warn!( - "OperatorCommitPubinReady for {instance_id}:{graph_id} is not implemented yet; graph status is {graph_status:?}" + + // compute the 96-byte guest pubin + let watchtower_challenge_init_txid = + SerializableTxid::from(graph.watchtower_challenge_init.tx().compute_txid()); + let num_watchtowers = graph.parameters.watchtower_pubkeys.len(); + let (challenge_txids, included_watchtowers_bits) = + get_watchtower_challenge_info(ctx.btc_client, &watchtower_challenge_init_txid, num_watchtowers) + .await?; + let (btc_best_block_hash, included_watchtowers_bitmap) = + compute_operator_pubin_blockhash_and_bitmap( + ctx.btc_client, + &challenge_txids, + &included_watchtowers_bits, + ) + .await?; + let guest_pubin = build_operator_guest_pubin( + &btc_best_block_hash, + &graph.parameters.pubin_disprove_constant, + &included_watchtowers_bitmap, ); + + // sign and broadcast + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let commit_pubin_wots_sk = operator_master_key.commit_pubin_wots_keypair_for_graph(graph_id).0; + let signed_input = operator_sign_commit_pubin(&graph, &commit_pubin_wots_sk, &guest_pubin)?; + let connector_e_amount = graph + .watchtower_challenge_init + .connector_e_input() + .map_err(|e| anyhow!("failed to get connector-e input: {e}"))? + .amount; + let operator_keypair = operator_master_key.master_keypair(); + build_sign_and_broadcast_tx( + ctx.btc_client, + operator_keypair, + vec![signed_input], + connector_e_amount, + vec![], + ) + .await?; + Ok(()) } @@ -3634,6 +3669,7 @@ async fn handle_operator_commit_pubin_timeout_verifier( Ok(()) } + // after the watchtower challenge flow is complete, build proof and broadcast Assert transaction. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_assert_ready_operator( @@ -3651,7 +3687,7 @@ async fn handle_assert_ready_operator( Some(v) => v, None => return Ok(()), }; - let (operator_wrapper_proof, wait_secs) = get_operator_wrapper_proof( + let (operator_proof, wait_secs) = get_operator_proof( ctx.local_db, ctx.http_client, &graph, @@ -3669,15 +3705,23 @@ async fn handle_assert_ready_operator( return Ok(()); } - let Some(operator_wrapper_proof) = operator_wrapper_proof else { + let Some(operator_proof) = operator_proof else { return Ok(()); }; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let assert_secret_key = operator_master_key.assert_wots_keypair_for_graph(graph_id).0; - let assert_witness = build_assert_witness(&operator_wrapper_proof.proof, &assert_secret_key)?; + + if operator_proof.public_inputs.len() != 2 { + bail!( + "operator proof has {} public inputs; expected 2", + operator_proof.public_inputs.len() + ); + } + let dynamic_input = operator_proof.public_inputs[1]; + let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; let mut asserted_wrapper_proof = Vec::new(); - operator_wrapper_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; + operator_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; let operator_state = setup_state @@ -3706,6 +3750,61 @@ async fn handle_assert_ready_operator( Ok(()) } +/// Recovers a `BabeChallengeAssertWitness` from the on-chain challenge assert tx. +fn recover_challenge_assert_witness( + challenge_assert_tx: &bitcoin::Transaction, + verifier_index: usize, +) -> Result { + let txin = challenge_assert_tx + .input + .first() + .ok_or_else(|| anyhow!("challenge assert tx has no input"))?; + let items: Vec> = txin.witness.to_vec(); + let expected_len = 2 * WOTS_SIG_COUNT + goat::assert_scripts::INPUT_WIRE_NUM + 2; + if items.len() != expected_len { + bail!( + "unexpected challenge assert witness length: {} (expected {expected_len})", + items.len() + ); + } + let mut bitcoin_witness = bitcoin::Witness::new(); + for item in &items[..2 * WOTS_SIG_COUNT] { + bitcoin_witness.push(item); + } + let wots_sig = Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec(); + let input_labels: Vec<[u8; 16]> = items + [2 * WOTS_SIG_COUNT..2 * WOTS_SIG_COUNT + goat::assert_scripts::INPUT_WIRE_NUM] + .iter() + .map(|item| item.as_slice().try_into()) + .collect::>() + .map_err(|_| anyhow!("challenge assert label item is not 16 bytes"))?; + Ok(BabeChallengeAssertWitness { + verifier_index, + witness: ChallengeAssertWitnessRaw { input_labels, wots_sig }, + }) +} + +/// Collects the on-chain ACK TxIns from all watchtowers that submitted a challenge_ack. +/// Only connectors that are already spent are included; unspent ones are skipped. +async fn collect_ack_txins( + btc_client: &BTCClient, + wci_txid: &Txid, + num_watchtowers: usize, +) -> Result> { + let mut ack_txins = Vec::new(); + for i in 0..num_watchtowers { + let ack_vout = output_topology::watchtower_challenge_init::ack_connector(i) as u64; + if let Some(ack_txid) = outpoint_spent_txid(btc_client, wci_txid, ack_vout).await? { + if let Some(ack_tx) = btc_client.get_tx(&ack_txid).await? { + if let Some(txin) = ack_tx.input.first().cloned() { + ack_txins.push(txin); + } + } + } + } + Ok(ack_txins) +} + // verify Operator DynamicPublicInput and Proof; broadcast PubinDisprove or ChallengeAssert as needed. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_assert_sent_verifier( @@ -3713,9 +3812,8 @@ async fn handle_assert_sent_verifier( instance_id: Uuid, graph_id: Uuid, assert_txid: Txid, - assert_witness: &Option, + assert_witness: &Option, ) -> Result<()> { - // TODO: check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction let (graph, _graph_status, _graph_sub_status) = match refresh_graph_status(ctx, instance_id, graph_id, None, GraphStatus::Challenge).await? { @@ -3729,13 +3827,6 @@ async fn handle_assert_sent_verifier( ); return Ok(()); }; - let Some(assert_witness) = assert_witness else { - // TODO(gc-v2): recover BabeAssertWitness from the assert tx witness and graph. - tracing::warn!( - "Ignore AssertSent for {instance_id}:{graph_id}: assert witness is not available in message" - ); - return Ok(()); - }; let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); let verifier_pubkey = verifier_master_key.master_keypair().public_key().into(); @@ -3749,6 +3840,86 @@ async fn handle_assert_sent_verifier( }; validate_verifier_slot(&graph, verifier_index)?; + // check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction + if let Ok(connector_e_input) = graph.watchtower_challenge_init.connector_e_input() { + let outpoint = connector_e_input.outpoint; + if let Some(commit_pubin_txid) = + outpoint_spent_txid(ctx.btc_client, &outpoint.txid, outpoint.vout as u64).await? + { + if let Some(commit_pubin_tx) = ctx.btc_client.get_tx(&commit_pubin_txid).await? { + if let Some(commit_pubin_txin) = commit_pubin_tx.input.first() { + let assert_txin = assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; + let wci_txid = graph.watchtower_challenge_init.tx().compute_txid(); + let num_watchtowers = graph.parameters.watchtower_ack_hashlocks.len(); + let ack_txins = collect_ack_txins(ctx.btc_client, &wci_txid, num_watchtowers).await?; + match validate_pubin_disprove(&graph, commit_pubin_txin, assert_txin, &ack_txins) { + Ok(Some((witness_data, _))) => { + // build pubin disprove Tx + let pubin_disprove_txin = + build_pubin_disprove_txin(&graph, witness_data)?; + let pubin_disprove_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![pubin_disprove_txin], + output: vec![goat::scripts::p2a_output()], + }; + broadcast_nonstandard_tx(ctx.btc_client, &pubin_disprove_tx).await?; + let message_content = + GOATMessageContent::DisproveSent(DisproveSent { + instance_id, + graph_id, + disprove_type: DisproveTxType::PubinDisprove, + index: verifier_index, + challenge_start_txid: None, + challenge_finish_txid: pubin_disprove_tx.compute_txid(), + }); + send_to_peer( + ctx.swarm, + GOATMessage::new(Actor::Committee, message_content), + ) + .await?; + return Ok(()); + } + Ok(None) => tracing::debug!( + "PubinDisprove invalid for {instance_id}:{graph_id}: operator pubin consistent, proceeding to ChallengeAssert" + ), + Err(e) => tracing::warn!( + "PubinDisprove check failed for {instance_id}:{graph_id}: {e}, proceeding to ChallengeAssert" + ), + } + } + } + } + } + + // recover TxAssertWitness from the assert tx witness and graph. + let assert_witness_recovered: TxAssertWitness; + let assert_witness: &TxAssertWitness = match assert_witness { + Some(w) => w, + None => { + let raw_witness = graph + .connector_c() + .extract_leaf_1_raw_witness( + assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?, + ) + .map_err(|e| anyhow!("failed to extract WOTS signature from assert tx: {e}"))?; + let mut bitcoin_witness = bitcoin::Witness::new(); + for item in &raw_witness { + bitcoin_witness.push(item); + } + assert_witness_recovered = TxAssertWitness { + wots_sig: Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec(), + }; + &assert_witness_recovered + } + }; + let Some(saved_verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .and_then(|state| state.verifier) else { @@ -3764,18 +3935,19 @@ async fn handle_assert_sent_verifier( return Ok(()); } let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE challenge")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let challenge_witness = build_real_challenge_assert_witness( &saved_verifier_state.private_state, &saved_verifier_state.setup_package, &saved_verifier_state.finalized_indices, &vk, - &public_inputs, + static_input, &graph.parameters.operator_assert_wots_pubkey, assert_witness, verifier_index, )?; let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .witness .input_labels .iter() .map(|label| label.to_vec()) @@ -3825,16 +3997,7 @@ async fn handle_challenge_assert_sent_operator( Some(v) => v, None => return Ok(()), }; - let Some(challenge_witness) = challenge_witness else { - // TODO(gc-v2): recover BabeChallengeAssertWitness from the challenge tx witness and graph. - tracing::warn!( - "Ignore ChallengeAssertSent for {instance_id}:{graph_id}: challenge witness is not available in message" - ); - return Ok(()); - }; - validate_verifier_slot(&graph, verifier_index)?; - validate_challenge_witness_index(verifier_index, challenge_witness)?; validate_expected_challenge_assert_txid(&graph, verifier_index, challenge_assert_txid)?; let Some(challenge_assert_tx) = ctx.btc_client.get_tx(&challenge_assert_txid).await? else { @@ -3848,7 +4011,19 @@ async fn handle_challenge_assert_sent_operator( return Ok(()); }; + let challenge_witness_recovered: BabeChallengeAssertWitness; + let challenge_witness: &BabeChallengeAssertWitness = match challenge_witness { + Some(w) => w, + None => { + challenge_witness_recovered = + recover_challenge_assert_witness(&challenge_assert_tx, verifier_index)?; + &challenge_witness_recovered + } + }; + validate_challenge_witness_index(verifier_index, challenge_witness)?; + let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .witness .input_labels .iter() .map(|label| label.to_vec()) @@ -3904,15 +4079,18 @@ async fn handle_challenge_assert_sent_operator( .ok_or_else(|| anyhow!("missing asserted wrapper proof for graph {graph_id}"))?; let proof = Groth16Proof::deserialize_compressed(proof_bytes.as_slice()) .context("deserialize asserted wrapper proof")?; + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE wrongly challenged")?; + let (_, dyn_pubin) = TxAssertWitness { wots_sig: challenge_witness.witness.wots_sig.clone() } + .recover_pi1_xd_without_verify() + .ok_or_else(|| anyhow!("cannot recover dynamic input from challenge witness WOTS signature"))?; let wrongly_challenged_witness = - recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof)?; - // TODO: The wrongly-challenged script accepts any one finalized-message preimage. - let final_msg = wrongly_challenged_witness - .final_msgs - .first() - .ok_or_else(|| anyhow!("wrongly challenged witness has no final message preimage"))?; - let (wrongly_challenged_input, _amount) = - operator_sign_wrongly_challenged(&graph, verifier_index, final_msg)?; + recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof, vk, dyn_pubin)?; + let (wrongly_challenged_input, _amount) = operator_sign_wrongly_challenged( + &graph, + verifier_index, + &wrongly_challenged_witness.final_msg, + )?; + let wrongly_challenged_tx = bitcoin::Transaction { version: bitcoin::transaction::Version(2), lock_time: bitcoin::absolute::LockTime::ZERO, @@ -4546,7 +4724,7 @@ async fn handle_response_node_info( #[cfg(test)] mod tests { use std::str::FromStr; - + use bitcoin::hashes::Hash; use bitvm_lib::babe_adapter::{ BABE_M_CC, BabeProverState, build_setup_package, open_and_solder, }; @@ -4562,7 +4740,9 @@ mod tests { let package = build_setup_package(BABE_M_CC + 1).unwrap(); let selected = (0..BABE_M_CC).collect::>(); let (_, finalized, soldering) = open_and_solder(&package, &selected).unwrap(); - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()).unwrap(); + 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).unwrap(); let prover_state = BabeProverState { package: package.clone(), finalized, @@ -4665,4 +4845,144 @@ mod tests { assert!(restored.candidates[0].prover_state.is_none()); } + + // ── collect_ack_txins ───────────────────────────────────────────────────── + + fn make_txid(byte: u8) -> Txid { + Txid::from_byte_array([byte; 32]) + } + + fn make_confirmed_ack_tx(ack_txid: Txid, wci_txid: Txid, ack_vout: u32) -> esplora_client::Tx { + use esplora_client::{TxStatus, Vin}; + let preimage = b"test-ack-preimage".to_vec(); + esplora_client::Tx { + txid: ack_txid, + version: 2, + locktime: 0, + vin: vec![Vin { + txid: wci_txid, + vout: ack_vout, + prevout: None, + scriptsig: bitcoin::ScriptBuf::default(), + witness: vec![preimage], + sequence: 1, + is_coinbase: false, + }], + vout: vec![], + size: 1, + weight: 1, + status: TxStatus { + confirmed: true, + block_height: Some(800_000), + block_hash: None, + block_time: Some(1_000_000), + }, + fee: 0, + } + } + + #[tokio::test] + async fn collect_ack_txins_returns_only_spent_connectors() { + let (btc_client, mock) = client::btc_chain::BTCClient::new_mock_client(); + let wci_txid = make_txid(0x00); + let ack_txid_0 = make_txid(0x01); + let num_watchtowers = 3; + + // Watchtower 0 ACKed + let vout_0 = output_topology::watchtower_challenge_init::ack_connector(0) as u32; + mock.set_tx(ack_txid_0, make_confirmed_ack_tx(ack_txid_0, wci_txid, vout_0)); + + // Watchtower 1 did NOT ACK (connector unspent — nothing registered) + + // Watchtower 2 ACKed + let ack_txid_2 = make_txid(0x02); + let vout_2 = output_topology::watchtower_challenge_init::ack_connector(2) as u32; + mock.set_tx(ack_txid_2, make_confirmed_ack_tx(ack_txid_2, wci_txid, vout_2)); + + let txins = collect_ack_txins(&btc_client, &wci_txid, num_watchtowers).await.unwrap(); + + assert_eq!(txins.len(), 2); + assert_eq!(txins[0].previous_output.vout, vout_0); + assert_eq!(txins[1].previous_output.vout, vout_2); + } + + // ── recover_challenge_assert_witness ────────────────────────────────────── + + fn build_challenge_assert_tx( + sig: &::Signature, + labels: &[[u8; 16]], + ) -> bitcoin::Transaction { + let mut witness = bitcoin::Witness::new(); + let raw_wots = Wots96::signature_to_raw_witness(sig); + for item in raw_wots.iter() { + witness.push(item); + } + for label in labels { + witness.push(label.as_slice()); + } + witness.push(&[0xde, 0xad]); // placeholder script + witness.push(&[0xbe, 0xef]); // placeholder control block + bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn { witness, ..Default::default() }], + output: vec![], + } + } + + #[test] + fn recover_challenge_assert_witness_extracts_wots_sig_and_labels() { + use bitvm_lib::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("test-challenge-witness"); + let msg = [0x42u8; 96]; + let sig = Wots96::sign(&sk, &msg); + + let labels: Vec<[u8; 16]> = (0..goat::assert_scripts::INPUT_WIRE_NUM) + .map(|i| [(i % 256) as u8; 16]) + .collect(); + + let tx = build_challenge_assert_tx(&sig, &labels); + let verifier_index = 3; + let recovered = recover_challenge_assert_witness(&tx, verifier_index).unwrap(); + + assert_eq!(recovered.verifier_index, verifier_index); + assert_eq!(recovered.witness.wots_sig.len(), WOTS_SIG_COUNT); + assert_eq!(recovered.witness.input_labels.len(), goat::assert_scripts::INPUT_WIRE_NUM); + + let mut bw = bitcoin::Witness::new(); + for item in Wots96::signature_to_raw_witness(&sig).iter() { + bw.push(item); + } + assert_eq!(recovered.witness.wots_sig, Wots96::raw_witness_to_signature(&bw).to_vec()); + + // labels are correctly extracted + for (i, label) in recovered.witness.input_labels.iter().enumerate() { + assert_eq!(label[0], (i % 256) as u8, "label {i} mismatch"); + } + } + + #[test] + fn recover_challenge_assert_witness_rejects_wrong_length() { + use bitvm_lib::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("test-wrong-len"); + let sig = Wots96::sign(&sk, &[0u8; 96]); + let labels: Vec<[u8; 16]> = vec![[0u8; 16]; goat::assert_scripts::INPUT_WIRE_NUM]; + + // one label short + let mut short_labels = labels.clone(); + short_labels.pop(); + let bad_tx = build_challenge_assert_tx(&sig, &short_labels); + assert!(recover_challenge_assert_witness(&bad_tx, 0).is_err()); + + // no inputs at all + let empty_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + assert!(recover_challenge_assert_witness(&empty_tx, 0).is_err()); + } } diff --git a/node/src/utils.rs b/node/src/utils.rs index 9d9f6a7b..08c6c0f0 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -25,7 +25,8 @@ use bitcoin::{ XOnlyPublicKey, }; use bitcoin_light_client_circuit::{ - VK_HASH_SIZE, build_watchtower_commitment, wrapper_public_values, + VK_HASH_SIZE, build_watchtower_commitment, decode_operator_public_outputs, + wrapper_public_values, }; use bitvm::treepp::*; use bitvm_lib::actors::Actor; @@ -116,6 +117,16 @@ pub type VerifyingKey = ark_groth16::VerifyingKey; pub type Groth16Proof = ark_groth16::Proof; pub type PublicInputs = Vec; +#[derive(Clone)] +pub struct ValidatedOperatorProof { + pub proof: Groth16Proof, + pub public_inputs: PublicInputs, + pub verifying_key: VerifyingKey, + pub public_values: Vec, + pub vk_hash: String, + pub zkm_version: String, +} + #[derive(Clone)] pub struct ValidatedOperatorWrapperProof { pub proof: Groth16Proof, @@ -126,6 +137,14 @@ pub struct ValidatedOperatorWrapperProof { pub zkm_version: String, } +#[derive(Clone)] +pub struct OperatorStatement { + pub static_input: ark_bn254::Fr, + pub vk_hash: [u8; 32], + pub zkm_version: String, + pub constant: [u8; 32], +} + #[derive(Clone)] pub struct OperatorWrapperStatement { pub public_values: Vec, @@ -1957,11 +1976,83 @@ pub async fn get_watchtower_challenge_info( } Ok((challenge_txids, included_watchtowers)) } + +/// Returns `(btc_best_block_hash, included_watchtowers_bitmap)` from already-fetched +/// `get_watchtower_challenge_info` output. +pub async fn compute_operator_pubin_blockhash_and_bitmap( + btc_client: &BTCClient, + challenge_txids: &[Option], + included_watchtowers_bits: &[bool], +) -> Result<([u8; 32], [u8; 32])> { + let btc_best_block_hash = { + let mut largest: Option<(u32, BlockHash)> = None; + for txid in challenge_txids.iter().flatten() { + let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; + let (height, hash) = match (status.block_height, status.block_hash) { + (Some(height), Some(hash)) => (height, hash), + _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), + }; + if largest.is_none_or(|(h, _)| height > h) { + largest = Some((height, hash)); + } + } + largest + .map(|(_, hash)| hash.to_byte_array()) + .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx available"))? + }; + + let mut included_watchtowers = [0u8; 32]; + for (i, &included) in included_watchtowers_bits.iter().enumerate() { + if included && i < 256 { + included_watchtowers[i / 8] |= 1 << (i % 8); + } + } + + Ok((btc_best_block_hash, included_watchtowers)) +} + +/// Assembles the 96-byte guest pubin: +pub fn build_operator_guest_pubin( + btc_best_block_hash: &[u8; 32], + pubin_disprove_constant: &[u8; 32], + included_watchtowers: &[u8; 32], +) -> [u8; 96] { + let mut pubin = [0u8; 96]; + pubin[0..32].copy_from_slice(btc_best_block_hash); + pubin[32..64].copy_from_slice(pubin_disprove_constant); + pubin[64..96].copy_from_slice(included_watchtowers); + pubin +} + fn load_part_stark_vk_for_zkm_version(zkm_version: &str) -> Result> { catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec())) .map_err(|_| anyhow!("failed to load part_stark_vk for zkm_version {zkm_version}")) } +fn combined_operator_vk_hash(operator_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { + if !operator_vk_hash.starts_with("0x") { + bail!("configured operator vk hash must use 0x-prefixed Ziren encoding"); + } + let raw_vk_hash = decode_zkm_vkey_hash(operator_vk_hash) + .map_err(|e| anyhow!("invalid configured operator vk hash: {e:?}"))?; + let part_vk: PartStarkVerifyingKey = + bincode::deserialize(&load_part_stark_vk_for_zkm_version(zkm_version)?) + .context("deserialize operator partial STARK verifying key")?; + let base = Bn254Fr::from_canonical_u32(256); + let mut field_hash = Bn254Fr::ZERO; + for byte in raw_vk_hash { + field_hash = field_hash * base + Bn254Fr::from_canonical_u32(byte as u32); + } + let combined = zkm_recursion_core::hash_vkey_with_part_vk(&part_vk, field_hash); + let bytes = combined.as_canonical_biguint().to_bytes_be(); + if bytes.len() > 32 { + bail!("combined operator verifying key hash exceeds BN254 field encoding"); + } + let mut encoded = [0u8; 32]; + encoded[32 - bytes.len()..].copy_from_slice(&bytes); + Ok(encoded) +} + fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { if !wrapper_vk_hash.starts_with("0x") { bail!("configured wrapper vk hash must use 0x-prefixed Ziren encoding"); @@ -1986,6 +2077,15 @@ fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result< Ok(encoded) } +pub fn derive_operator_statement(graph_id: Uuid) -> Result { + let vk_hash = get_operator_vk_hash()?; + let zkm_version = get_operator_zkm_version()?; + let combined_hash = combined_operator_vk_hash(&format!("0x{}", hex::encode(vk_hash)), &zkm_version)?; + let static_input = load_ark_public_inputs_from_bytes(&combined_hash, &[0u8; 32])[0]; + let constant = hash_operator_constant(*graph_id.as_bytes(), get_genesis_sequencer_commit_id()); + Ok(OperatorStatement { static_input, vk_hash, zkm_version, constant }) +} + pub fn derive_operator_wrapper_statement(graph_id: Uuid) -> Result { let public_values = wrapper_public_values( get_operator_vk_hash()?, @@ -2043,22 +2143,13 @@ pub async fn get_operator_wrapper_proof( return Ok((None, get_operator_proof_wait_secs())); } }; - let operator_committed_blockhash = { - let mut largest: Option<(u32, BlockHash)> = None; - for txid in watchtower_challenge_txids.iter().flatten() { - let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; - let (height, hash) = match (status.block_height, status.block_hash) { - (Some(height), Some(hash)) => (height, hash), - _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), - }; - if largest.is_none_or(|(largest_height, _)| height > largest_height) { - largest = Some((height, hash)); - } - } - largest - .map(|(_, hash)| hash.to_string()) - .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx is available"))? - }; + let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( + btc_client, + &watchtower_challenge_txids, + &included_watchtowers, + ) + .await?; + let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); let base_url = Url::parse( &get_proof_build_rpc_host() @@ -2149,6 +2240,129 @@ pub async fn get_operator_wrapper_proof( 0, )) } + +/// Returns: +/// - `Ok(Some(OperatorProof), _)` if operator proof is available and valid +/// - `Ok(None, wait_secs)` if operator proof is not yet available +pub async fn get_operator_proof( + local_db: &LocalDB, + http_client: &HttpAsyncClient, + bitvm_graph: &BitvmGcGraph, + btc_client: &BTCClient, + instance_id: Uuid, + graph_id: Uuid, +) -> Result<(Option, usize)> { + let mut storage_processor = local_db.acquire().await?; + let Some(graph) = storage_processor.find_graph(&graph_id).await? else { + warn!("graph:{graph_id} not found"); + bail!("No graph in db"); + }; + drop(storage_processor); + + if graph.proceed_withdraw_height <= 0 { + warn!("graph {graph_id} proceed_withdraw_height <= 0, waiting to been updated"); + return Ok((None, get_operator_proof_wait_secs())); + } + + let watchtower_challenge_init_txid = graph + .watchtower_challenge_init_txid + .ok_or_else(|| anyhow::anyhow!("watchtower_challenge_init_txid is none"))?; + let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); + let (watchtower_challenge_txids, included_watchtowers) = match get_watchtower_challenge_info( + btc_client, + &watchtower_challenge_init_txid, + num_challenger, + ) + .await + { + Ok(info) => info, + Err(e) => { + warn!("Failed to get watchtower challenge info: {e}"); + return Ok((None, get_operator_proof_wait_secs())); + } + }; + let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( + btc_client, + &watchtower_challenge_txids, + &included_watchtowers, + ) + .await?; + let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); + + let base_url = Url::parse( + &get_proof_build_rpc_host() + .ok_or_else(|| anyhow::anyhow!("failed to get proof_build_rpc_host"))?, + )?; + let operator_url = base_url.join(NODES_OPERATOR_BASE)?; + let operator_response = http_client + .post_response_json::( + operator_url.as_str(), + &OperatorProofRequest { + instance_id: instance_id.to_string(), + graph_id: graph_id.to_string(), + operator_committed_blockhash, + execution_layer_block_number: graph.proceed_withdraw_height, + watchtower_challenge_txids, + included_watchtowers, + watchtower_challenge_init_txid: watchtower_challenge_init_txid.0.to_string(), + watchtower_challenge_pubkeys: bitvm_graph + .parameters + .watchtower_pubkeys + .iter() + .map(|pk| pk.public_key(secp256k1::Parity::Even).to_string()) + .collect(), + }, + ) + .await?; + + let Some(proof_data) = operator_response.proof_data else { + if let Some(error) = operator_response.error { + info!("operator proof is not ready for graph_id:{graph_id}: {error}"); + } + return Ok((None, get_operator_proof_wait_secs())); + }; + + let statement = derive_operator_statement(graph_id)?; + + let proof: ZKMProofWithPublicValues = bincode::deserialize(proof_data.proof.as_slice()) + .map_err(|err| anyhow!("failed to deserialize operator proof: {err}"))?; + + let operator_vk_hash_raw = decode_zkm_vkey_hash(&proof_data.vk) + .map_err(|e| anyhow!("invalid operator proof vk hash: {e:?}"))?; + if operator_vk_hash_raw != statement.vk_hash { + bail!("operator proof vk hash does not match configured operator identity"); + } + if proof.zkm_version != statement.zkm_version || proof_data.zkm_version != statement.zkm_version { + bail!("operator proof Ziren version does not match configured operator identity"); + } + + let outputs = decode_operator_public_outputs(&proof.public_values.to_vec(), statement.vk_hash) + .map_err(|e| anyhow!("invalid operator public outputs: {e}"))?; + if outputs.constant != statement.constant { + bail!("operator proof constant does not match graph setup"); + } + + let part_stark_vk = load_part_stark_vk_for_zkm_version(&proof.zkm_version)?; + let ark_proof = + convert_ark_imm_wrap_vk(&proof, &proof_data.vk, &IMM_GROTH16_VK_BYTES, &part_stark_vk) + .map_err(|e| anyhow!("failed to convert operator proof to ark format: {e}"))?; + if ark_proof.public_inputs[0] != statement.static_input { + bail!("operator proof static public input does not match graph setup statement"); + } + + Ok(( + Some(ValidatedOperatorProof { + proof: ark_proof.proof, + public_inputs: ark_proof.public_inputs.into(), + verifying_key: ark_proof.groth16_vk.into(), + public_values: proof.public_values.to_vec(), + vk_hash: proof_data.vk, + zkm_version: proof.zkm_version, + }), + 0, + )) +} + pub async fn verifier_force_skip_kickoff(client: &BTCClient, graph: &BitvmGcGraph) -> Result { let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); let verifier_master_keypair = verifier_master_key.master_keypair(); @@ -4773,3 +4987,121 @@ pub struct OperatorBabeSetupState { #[serde(default)] pub asserted_wrapper_proof: Option>, } + +#[cfg(test)] +mod commit_pubin_tests { + use super::*; + use bitcoin::BlockHash; + use client::btc_chain::BTCClient; + use esplora_client::{Tx, TxStatus, Vin}; + use store::SerializableTxid; + + fn make_txid(byte: u8) -> Txid { + Txid::from_slice(&[byte; 32]).unwrap() + } + + fn make_block_hash(byte: u8) -> BlockHash { + BlockHash::from_slice(&[byte; 32]).unwrap() + } + + fn create_confirmed_tx( + txid: Txid, + spends: &[(Txid, u32)], + height: u32, + block_hash: BlockHash, + ) -> Tx { + Tx { + txid, + version: 2, + locktime: 0, + vin: spends + .iter() + .map(|(prev_txid, vout)| Vin { + txid: *prev_txid, + vout: *vout, + prevout: None, + scriptsig: ScriptBuf::default(), + witness: vec![], + sequence: 1, + is_coinbase: false, + }) + .collect(), + vout: vec![], + size: 1, + weight: 1, + status: TxStatus { + confirmed: true, + block_height: Some(height), + block_hash: Some(block_hash), + block_time: Some(1_000_000), + }, + fee: 0, + } + } + + #[tokio::test] + async fn test_get_watchtower_challenge_info_partial_inclusion() { + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let watchtower_init_txid = make_txid(0x00); + let challenge_txid_wt0 = make_txid(0x01); + let block_hash = make_block_hash(0xAA); + + // watchtower 0 spent vout 0 of the init tx; watchtower 1 did not spend vout 1 + mock_adaptor.set_tx( + challenge_txid_wt0, + create_confirmed_tx(challenge_txid_wt0, &[(watchtower_init_txid, 0)], 100, block_hash), + ); + + let (txids, bits) = get_watchtower_challenge_info( + &btc_client, + &SerializableTxid::from(watchtower_init_txid), + 2, // 2 watchtowers + ) + .await + .unwrap(); + + assert_eq!(txids, vec![Some(challenge_txid_wt0.to_string()), None]); + assert_eq!(bits, vec![true, false]); + } + + #[tokio::test] + async fn test_compute_operator_pubin_blockhash_and_bitmap() { + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let init_txid = make_txid(0x00); + let challenge_txid_wt0 = make_txid(0x01); + let challenge_txid_wt2 = make_txid(0x02); + let block_hash_low = make_block_hash(0x10); + let block_hash_high = make_block_hash(0x20); + + // wt0 confirmed at height 100, wt2 at height 200 (highest) + mock_adaptor.set_tx( + challenge_txid_wt0, + create_confirmed_tx(challenge_txid_wt0, &[(init_txid, 0)], 100, block_hash_low), + ); + mock_adaptor.set_tx( + challenge_txid_wt2, + create_confirmed_tx(challenge_txid_wt2, &[(init_txid, 2)], 200, block_hash_high), + ); + + let challenge_txids = vec![ + Some(challenge_txid_wt0.to_string()), + None, + Some(challenge_txid_wt2.to_string()), + ]; + let bits = vec![true, false, true]; + + let (best_hash, bitmap) = + compute_operator_pubin_blockhash_and_bitmap(&btc_client, &challenge_txids, &bits) + .await + .unwrap(); + + // block at height 200 wins + assert_eq!(best_hash, block_hash_high.to_byte_array()); + + // bits 0 and 2 set → byte 0 = 0b0000_0101 + assert_eq!(bitmap[0], 0b0000_0101u8); + assert_eq!(&bitmap[1..], &[0u8; 31]); + } +} From c8e7751d2d1a6d25463ac43f48e7a6925329a1dd Mon Sep 17 00:00:00 2001 From: Blake Hau <0xblake.sg@gmail.com> Date: Tue, 23 Jun 2026 15:23:51 +0800 Subject: [PATCH 04/14] remove wrapper proofs (#406) * remove wrapper proofs * remove operator vk hash from public inputs --- Cargo.lock | 24 - Cargo.toml | 2 - circuits/README.md | 9 +- circuits/operator-proof/guest/src/main.rs | 6 +- circuits/operator-proof/host/src/lib.rs | 9 +- .../operator-wrapper-proof/guest/Cargo.lock | 6696 ----------------- .../operator-wrapper-proof/guest/Cargo.toml | 33 - .../operator-wrapper-proof/guest/src/main.rs | 39 - .../operator-wrapper-proof/host/Cargo.toml | 25 - circuits/operator-wrapper-proof/host/build.rs | 5 - .../operator-wrapper-proof/host/src/lib.rs | 302 - .../operator-wrapper-proof/host/src/main.rs | 24 - circuits/proof-builder/src/lib.rs | 85 +- circuits/run-operator-wrapper-proof.sh | 51 - .../bitcoin-light-client-circuit/src/lib.rs | 99 +- crates/bitvm-gc/src/babe_adapter.rs | 2 +- ...0260622000000_drop_wrapper_proof_table.sql | 1 + crates/store/src/localdb.rs | 296 +- crates/store/src/schema.rs | 24 - .../proof-builder-rpc/proof-builder.toml | 6 - node/src/env.rs | 12 - node/src/handle.rs | 24 +- node/src/rpc_service/routes.rs | 1 - node/src/utils.rs | 207 +- proof-builder-rpc/Cargo.toml | 1 - proof-builder-rpc/proof-builder.toml.example | 6 - proof-builder-rpc/src/api/mod.rs | 8 +- proof-builder-rpc/src/api/proof_handler.rs | 138 +- proof-builder-rpc/src/api/routes.rs | 2 - proof-builder-rpc/src/config.rs | 2 - proof-builder-rpc/src/task/mod.rs | 31 - proof-builder-rpc/src/task/wrapper_proof.rs | 259 - 32 files changed, 42 insertions(+), 8387 deletions(-) delete mode 100644 circuits/operator-wrapper-proof/guest/Cargo.lock delete mode 100644 circuits/operator-wrapper-proof/guest/Cargo.toml delete mode 100644 circuits/operator-wrapper-proof/guest/src/main.rs delete mode 100644 circuits/operator-wrapper-proof/host/Cargo.toml delete mode 100644 circuits/operator-wrapper-proof/host/build.rs delete mode 100644 circuits/operator-wrapper-proof/host/src/lib.rs delete mode 100644 circuits/operator-wrapper-proof/host/src/main.rs delete mode 100755 circuits/run-operator-wrapper-proof.sh create mode 100644 crates/store/migrations/20260622000000_drop_wrapper_proof_table.sql delete mode 100644 proof-builder-rpc/src/task/wrapper_proof.rs diff --git a/Cargo.lock b/Cargo.lock index 66f2c452..161e2f46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8646,29 +8646,6 @@ dependencies = [ "zkm-verifier", ] -[[package]] -name = "operator-wrapper-proof" -version = "0.4.0" -dependencies = [ - "anyhow", - "bincode", - "bitcoin", - "bitcoin-light-client-circuit", - "clap", - "dotenv", - "hex", - "proof-builder", - "serde", - "sha2 0.10.9", - "tokio", - "tracing", - "tracing-subscriber 0.3.23", - "zkm-build", - "zkm-primitives", - "zkm-prover", - "zkm-sdk", -] - [[package]] name = "option-ext" version = "0.2.0" @@ -9788,7 +9765,6 @@ dependencies = [ "header-chain-proof", "http-body-util", "operator-proof", - "operator-wrapper-proof", "prometheus-client", "proof-builder", "serde", diff --git a/Cargo.toml b/Cargo.toml index 340b22a4..0870d287 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,7 +23,6 @@ members = [ "circuits/state-chain-proof/host", "circuits/watchtower-proof/host", "circuits/operator-proof/host", - "circuits/operator-wrapper-proof/host", "circuits/proof-builder", ] default-members = ["node"] @@ -127,7 +126,6 @@ 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" } -operator-wrapper-proof = { path = "circuits/operator-wrapper-proof/host" } watchtower-proof = { path = "circuits/watchtower-proof/host" } proof-builder = { path = "circuits/proof-builder" } diff --git a/circuits/README.md b/circuits/README.md index 1f15c81a..103f2d27 100644 --- a/circuits/README.md +++ b/circuits/README.md @@ -242,18 +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" ``` -Then wrap the operator proof before the operator sends assert-commit. The Challenge script validates -the wrapper proof public inputs: - -``` -bash run-operator-wrapper-proof.sh "data/operator-proof/output.bin" "data/operator-wrapper-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 inside operator proof. -* operator-wrapper-proof: wraps operator proof and exposes `operator_vk_hash_raw`, raw 16-byte `graph_id`, and `genesis_sequencer_commit_txid` as public inputs for Challenge. * 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. diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index 960d8445..3563f489 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -18,6 +18,7 @@ pub fn main() { 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 + // 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(); @@ -29,9 +30,8 @@ pub fn main() { let operator_state_chain: StateChainCircuitInput = zkm_zkvm::io::read(); let spv_ss_commit: SPV = zkm_zkvm::io::read(); let operator_committed_blockhash: [u8; 32] = zkm_zkvm::io::read(); - let actual_operator_vk_hash: [u8; 32] = zkm_zkvm::io::read(); - let (btc_best_block_hash, constant, included_watchtowers, operator_vk_hash) = + let (btc_best_block_hash, constant, included_watchtowers) = bitcoin_light_client_circuit::propose_longest_chain( included_watchtowers, graph_id, @@ -46,11 +46,9 @@ pub fn main() { operator_state_chain, spv_ss_commit, operator_committed_blockhash, - actual_operator_vk_hash, ); zkm_zkvm::io::commit(&btc_best_block_hash); zkm_zkvm::io::commit(&constant); zkm_zkvm::io::commit(&included_watchtowers); - zkm_zkvm::io::commit(&operator_vk_hash); } diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 575b1172..2f37d1dd 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -6,7 +6,7 @@ use bitcoin::{ hashes::Hash, secp256k1::{PublicKey, XOnlyPublicKey}, }; -use bitcoin_light_client_circuit::{build_spv, zkm_vk_hash_to_raw}; +use bitcoin_light_client_circuit::build_spv; use bitcoin_script::script; use borsh::BorshDeserialize; use clap::Parser; @@ -400,9 +400,6 @@ impl ProofBuilder for OperatorProofBuilder { &bitcoin_block_headers, ); - let actual_operator_vk_hash = zkm_vk_hash_to_raw(self.verifying_key.bytes32().as_bytes()) - .map_err(anyhow::Error::msg)?; - // Generate the proofs let (proof, cycles, proving_time) = tracing::info_span!("generate proof").in_scope( || -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { @@ -426,7 +423,6 @@ impl ProofBuilder for OperatorProofBuilder { stdin.write(&state_chain_input); stdin.write(&spv_ss_commit); stdin.write(&operator_committed_blockhash.to_byte_array()); - stdin.write(&actual_operator_vk_hash); let elf_id = if ELF_ID.get().is_none() { ELF_ID @@ -494,11 +490,8 @@ mod tests { let proof: ZKMProofWithPublicValues = bincode::deserialize(&proof_bytes).unwrap(); let vk_hash = String::from_utf8(vk_bytes).unwrap(); - let operator_vk_hash = - bitcoin_light_client_circuit::zkm_vk_hash_to_raw(vk_hash.as_bytes()).unwrap(); let a = bitcoin_light_client_circuit::decode_operator_public_outputs( proof.public_values.as_slice(), - operator_vk_hash, ) .unwrap(); println!( diff --git a/circuits/operator-wrapper-proof/guest/Cargo.lock b/circuits/operator-wrapper-proof/guest/Cargo.lock deleted file mode 100644 index 332cd093..00000000 --- a/circuits/operator-wrapper-proof/guest/Cargo.lock +++ /dev/null @@ -1,6696 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "addchain" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e33f6a175ec6a9e0aca777567f9ff7c3deefc255660df887e7fa3585e9801d8" -dependencies = [ - "num-bigint 0.3.3", - "num-integer", - "num-traits", -] - -[[package]] -name = "addr2line" -version = "0.25.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b5d307320b3181d6d7954e663bd7c774a838b8220fe0593c86d9fb09f498b4b" -dependencies = [ - "gimli", -] - -[[package]] -name = "adler2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "aho-corasick" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" -dependencies = [ - "memchr", -] - -[[package]] -name = "allocator-api2" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" - -[[package]] -name = "alloy-chains" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84e0378e959aa6a885897522080a990e80eb317f1e9a222a604492ea50e13096" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "num_enum", - "serde", - "strum 0.27.2", -] - -[[package]] -name = "alloy-consensus" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "alloy-trie", - "alloy-tx-macros", - "auto_impl", - "c-kzg", - "derive_more 2.1.1", - "either", - "k256", - "once_cell", - "rand 0.8.6", - "secp256k1 0.30.0", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-consensus-any" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-eip2124" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741bdd7499908b3aa0b159bba11e71c8cddd009a2c2eb7a06e825f1ec87900a5" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "crc", - "serde", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-eip2930" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9441120fa82df73e8959ae0e4ab8ade03de2aaae61be313fbf5746277847ce25" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "borsh", - "serde", -] - -[[package]] -name = "alloy-eip7702" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2919c5a56a1007492da313e7a3b6d45ef5edc5d33416fdec63c0d7a2702a0d20" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "borsh", - "k256", - "serde", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-eips" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-eip2124", - "alloy-eip2930", - "alloy-eip7702", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "auto_impl", - "c-kzg", - "derive_more 2.1.1", - "either", - "serde", - "serde_with", - "sha2 0.10.9", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-evm" -version = "0.23.0" -source = "git+https://github.com/ziren-patches/evm?branch=patch-0.23.0#3cc30123cc6abb1c0da7e20318202821cab5d663" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-hardforks", - "alloy-primitives", - "alloy-sol-types", - "anyhow", - "auto_impl", - "derive_more 2.1.1", - "revm", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-genesis" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-serde", - "alloy-trie", - "serde", - "serde_with", -] - -[[package]] -name = "alloy-hardforks" -version = "0.4.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83ba208044232d14d4adbfa77e57d6329f51bc1acc21f5667bb7db72d88a0831" -dependencies = [ - "alloy-chains", - "alloy-eip2124", - "alloy-primitives", - "auto_impl", - "dyn-clone", -] - -[[package]] -name = "alloy-json-abi" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5513d5e6bd1cba6bdcf5373470f559f320c05c8c59493b6e98912fbe6733943f" -dependencies = [ - "alloy-primitives", - "alloy-sol-type-parser", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-json-rpc" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-primitives", - "alloy-sol-types", - "http", - "serde", - "serde_json", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "alloy-network" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-consensus", - "alloy-consensus-any", - "alloy-eips", - "alloy-json-rpc", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rpc-types-any", - "alloy-rpc-types-eth", - "alloy-serde", - "alloy-signer", - "alloy-sol-types", - "async-trait", - "auto_impl", - "derive_more 2.1.1", - "futures-utils-wasm", - "serde", - "serde_json", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-network-primitives" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-serde", - "serde", -] - -[[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" -dependencies = [ - "alloy-rlp", - "bytes", - "cfg-if", - "const-hex", - "derive_more 2.1.1", - "foldhash 0.2.0", - "hashbrown 0.16.1", - "indexmap 2.14.0", - "itoa", - "k256", - "keccak-asm", - "paste", - "proptest", - "rand 0.9.4", - "ruint", - "rustc-hash", - "serde", - "sha3", - "tiny-keccak", - "zkm-zkvm", -] - -[[package]] -name = "alloy-rlp" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" -dependencies = [ - "alloy-rlp-derive", - "arrayvec", - "bytes", -] - -[[package]] -name = "alloy-rlp-derive" -version = "0.3.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "alloy-rpc-types" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-primitives", - "alloy-rpc-types-eth", - "alloy-serde", - "serde", -] - -[[package]] -name = "alloy-rpc-types-any" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-consensus-any", - "alloy-rpc-types-eth", - "alloy-serde", -] - -[[package]] -name = "alloy-rpc-types-debug" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-primitives", - "derive_more 2.1.1", - "serde", - "serde_with", -] - -[[package]] -name = "alloy-rpc-types-engine" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "derive_more 2.1.1", - "strum 0.27.2", -] - -[[package]] -name = "alloy-rpc-types-eth" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-consensus", - "alloy-consensus-any", - "alloy-eips", - "alloy-network-primitives", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "alloy-sol-types", - "itertools 0.14.0", - "serde", - "serde_json", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-serde" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-primitives", - "serde", - "serde_json", -] - -[[package]] -name = "alloy-signer" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-primitives", - "async-trait", - "auto_impl", - "either", - "elliptic-curve", - "k256", - "thiserror 2.0.18", -] - -[[package]] -name = "alloy-sol-macro" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab81bab693da9bb79f7a95b64b394718259fdd7e41dceeced4cad57cb71c4f6a" -dependencies = [ - "alloy-sol-macro-expander", - "alloy-sol-macro-input", - "proc-macro-error2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "alloy-sol-macro-expander" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "489f1620bb7e2483fb5819ed01ab6edc1d2f93939dce35a5695085a1afd1d699" -dependencies = [ - "alloy-sol-macro-input", - "const-hex", - "heck", - "indexmap 2.14.0", - "proc-macro-error2", - "proc-macro2", - "quote", - "sha3", - "syn 2.0.117", - "syn-solidity", -] - -[[package]] -name = "alloy-sol-macro-input" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56cef806ad22d4392c5fc83cf8f2089f988eb99c7067b4e0c6f1971fc1cca318" -dependencies = [ - "const-hex", - "dunce", - "heck", - "macro-string", - "proc-macro2", - "quote", - "syn 2.0.117", - "syn-solidity", -] - -[[package]] -name = "alloy-sol-type-parser" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6df77fea9d6a2a75c0ef8d2acbdfd92286cc599983d3175ccdc170d3433d249" -dependencies = [ - "serde", - "winnow 0.7.15", -] - -[[package]] -name = "alloy-sol-types" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70319350969a3af119da6fb3e9bddb1bce66c9ea933600cb297c8b1850ad2a3c" -dependencies = [ - "alloy-json-abi", - "alloy-primitives", - "alloy-sol-macro", - "serde", -] - -[[package]] -name = "alloy-trie" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f14b5d9b2c2173980202c6ff470d96e7c5e202c65a9f67884ad565226df7fbb" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "derive_more 2.1.1", - "nybbles", - "serde", - "smallvec", - "thiserror 2.0.18", - "tracing", -] - -[[package]] -name = "alloy-tx-macros" -version = "1.0.41" -source = "git+https://github.com/ziren-patches/alloy?branch=patch-1.0.41#08557d4e5f4ca766194a45c1925d8f1625258793" -dependencies = [ - "alloy-primitives", - "darling 0.21.3", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "android_system_properties" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" -dependencies = [ - "libc", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "anstream" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" -dependencies = [ - "anstyle", - "anstyle-parse", - "anstyle-query", - "anstyle-wincon", - "colorchoice", - "is_terminal_polyfill", - "utf8parse", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "anstyle-parse" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" -dependencies = [ - "anstyle", - "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "anyhow" -version = "1.0.102" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" - -[[package]] -name = "ark-bls12-381" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df4dcc01ff89867cd86b0da835f23c3f02738353aaee7dde7495af71363b8d5" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", -] - -[[package]] -name = "ark-bn254" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" -dependencies = [ - "ark-ec", - "ark-ff 0.5.0", - "ark-std 0.5.0", -] - -[[package]] -name = "ark-ec" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-poly", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.5", - "itertools 0.13.0", - "num-bigint 0.4.6", - "num-integer", - "num-traits", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b3235cc41ee7a12aaaf2c575a2ad7b46713a8a50bda2fc3b003a04845c05dd6" -dependencies = [ - "ark-ff-asm 0.3.0", - "ark-ff-macros 0.3.0", - "ark-serialize 0.3.0", - "ark-std 0.3.0", - "derivative", - "num-bigint 0.4.6", - "num-traits", - "paste", - "rustc_version 0.3.3", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec847af850f44ad29048935519032c33da8aa03340876d351dfab5660d2966ba" -dependencies = [ - "ark-ff-asm 0.4.2", - "ark-ff-macros 0.4.2", - "ark-serialize 0.4.2", - "ark-std 0.4.0", - "derivative", - "digest 0.10.7", - "itertools 0.10.5", - "num-bigint 0.4.6", - "num-traits", - "paste", - "rustc_version 0.4.1", - "zeroize", -] - -[[package]] -name = "ark-ff" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" -dependencies = [ - "ark-ff-asm 0.5.0", - "ark-ff-macros 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "educe", - "itertools 0.13.0", - "num-bigint 0.4.6", - "num-traits", - "paste", - "zeroize", -] - -[[package]] -name = "ark-ff-asm" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db02d390bf6643fb404d3d22d31aee1c4bc4459600aef9113833d17e786c6e44" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed4aa4fe255d0bc6d79373f7e31d2ea147bcf486cba1be5ba7ea85abdb92348" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-asm" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" -dependencies = [ - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ark-ff-macros" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fd794a08ccb318058009eefdf15bcaaaaf6f8161eb3345f907222bac38b20" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abe79b0e4288889c4574159ab790824d0033b9fdcb2a112a3182fac2e514565" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "ark-ff-macros" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" -dependencies = [ - "num-bigint 0.4.6", - "num-traits", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ark-poly" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" -dependencies = [ - "ahash", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "ark-std 0.5.0", - "educe", - "fnv", - "hashbrown 0.15.5", -] - -[[package]] -name = "ark-serialize" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6c2b318ee6e10f8c2853e73a83adc0ccb88995aa978d8a3408d492ab2ee671" -dependencies = [ - "ark-std 0.3.0", - "digest 0.9.0", -] - -[[package]] -name = "ark-serialize" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "adb7b85a02b83d2f22f89bd5cac66c9c89474240cb6207cb1efc16d098e822a5" -dependencies = [ - "ark-std 0.4.0", - "digest 0.10.7", - "num-bigint 0.4.6", -] - -[[package]] -name = "ark-serialize" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" -dependencies = [ - "ark-serialize-derive", - "ark-std 0.5.0", - "arrayvec", - "digest 0.10.7", - "num-bigint 0.4.6", -] - -[[package]] -name = "ark-serialize-derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "ark-std" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" -dependencies = [ - "num-traits", - "rand 0.8.6", -] - -[[package]] -name = "ark-std" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" -dependencies = [ - "num-traits", - "rand 0.8.6", -] - -[[package]] -name = "ark-std" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" -dependencies = [ - "num-traits", - "rand 0.8.6", -] - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "aurora-engine-modexp" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518bc5745a6264b5fd7b09dffb9667e400ee9e2bbe18555fac75e1fe9afa0df9" -dependencies = [ - "hex", - "num", -] - -[[package]] -name = "auto_impl" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffdcb70bdbc4d478427380519163274ac86e52916e10f0a8889adf0f96d3fee7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "autocfg" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" - -[[package]] -name = "backtrace" -version = "0.3.76" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb531853791a215d7c62a30daf0dde835f381ab5de4589cfe7c649d2cbe92bd6" -dependencies = [ - "addr2line", - "cfg-if", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", - "serde", - "windows-link", -] - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base58ck" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c8d66485a3a2ea485c1913c4572ce0256067a5377ac8c75c4960e1cda98605f" -dependencies = [ - "bitcoin-internals", - "bitcoin_hashes", -] - -[[package]] -name = "base64" -version = "0.21.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" - -[[package]] -name = "bech32" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32637268377fc7b10a8c6d51de3e7fba1ce5dd371a96e342b34e6078db558e7f" - -[[package]] -name = "bincode" -version = "1.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" -dependencies = [ - "serde", -] - -[[package]] -name = "bit-set" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" - -[[package]] -name = "bitcoin" -version = "0.32.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e499f9fc0407f50fe98af744ab44fa67d409f76b6772e1689ec8485eb0c0f66" -dependencies = [ - "base58ck", - "bech32", - "bitcoin-internals", - "bitcoin-io", - "bitcoin-units", - "bitcoin_hashes", - "hex-conservative", - "hex_lit", - "secp256k1 0.29.0", - "serde", -] - -[[package]] -name = "bitcoin-internals" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30bdbe14aa07b06e6cfeffc529a1f099e5fbe249524f8125358604df99a4bed2" -dependencies = [ - "serde", -] - -[[package]] -name = "bitcoin-io" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dee39a0ee5b4095224a0cfc6bf4cc1baf0f9624b96b367e53b66d974e51d953" - -[[package]] -name = "bitcoin-light-client-circuit" -version = "0.4.0" -dependencies = [ - "alloy-primitives", - "base64 0.21.7", - "bincode", - "bitcoin", - "commit-chain", - "guest-executor", - "header-chain", - "hex", - "prost-build", - "revm-database-interface", - "serde", - "serde_json", - "state-chain", - "tendermint", - "tendermint-light-client-verifier", - "tracing", - "verifier", - "zkm-primitives", - "zkm-verifier", - "zkm-zkvm", -] - -[[package]] -name = "bitcoin-units" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346568ebaab2918487cea76dd55dae13c27bb618cdb737c952e69eb2017c4118" -dependencies = [ - "bitcoin-internals", - "serde", -] - -[[package]] -name = "bitcoin_hashes" -version = "0.14.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26ec84b80c482df901772e931a9a681e26a1b9ee2302edeff23cb30328745c8b" -dependencies = [ - "bitcoin-io", - "hex-conservative", - "serde", -] - -[[package]] -name = "bitflags" -version = "2.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" -dependencies = [ - "serde_core", -] - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "serde", - "tap", - "wyz", -] - -[[package]] -name = "blake2" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "blake2b_simd" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b79834656f71332577234b50bfc009996f7449e0c056884e6a02492ded0ca2f3" -dependencies = [ - "arrayref", - "arrayvec", - "constant_time_eq", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array 0.14.9", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array 0.14.9", -] - -[[package]] -name = "bls12_381" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3c196a77437e7cc2fb515ce413a6401291578b5afc8ecb29a3c7ab957f05941" -dependencies = [ - "ff 0.12.1", - "group 0.12.1", - "pairing 0.22.0", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "bls12_381" -version = "0.8.0" -source = "git+https://github.com/ziren-patches/bls12_381?branch=patch-0.8.0#bcf9397426410acc171ad625416b508dc913be8e" -dependencies = [ - "cfg-if", - "ff 0.13.1", - "group 0.13.0", - "pairing 0.23.0", - "rand_core 0.6.4", - "subtle", - "zkm-lib", -] - -[[package]] -name = "blst" -version = "0.3.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcdb4c7013139a150f9fc55d123186dbfaba0d912817466282c73ac49e71fb45" -dependencies = [ - "cc", - "glob", - "threadpool", - "zeroize", -] - -[[package]] -name = "borsh" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfd1e3f8955a5d7de9fab72fc8373fade9fb8a703968cb200ae3dc6cf08e185a" -dependencies = [ - "borsh-derive", - "bytes", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfcfdc083699101d5a7965e49925975f2f55060f94f9a05e7187be95d530ca59" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "bumpalo" -version = "3.20.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" - -[[package]] -name = "byte-slice-cast" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" - -[[package]] -name = "bytemuck" -version = "1.25.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8efb64bd706a16a1bdde310ae86b351e4d21550d98d056f22f8a7f7a2183fec" -dependencies = [ - "bytemuck_derive", -] - -[[package]] -name = "bytemuck_derive" -version = "1.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" -dependencies = [ - "serde", -] - -[[package]] -name = "c-kzg" -version = "2.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6648ed1e4ea8e8a1a4a2c78e1cda29a3fd500bc622899c340d8525ea9a76b24a" -dependencies = [ - "blst", - "cc", - "glob", - "hex", - "libc", - "once_cell", - "serde", -] - -[[package]] -name = "cc" -version = "1.2.61" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" -dependencies = [ - "find-msvc-tools", - "jobserver", - "libc", - "shlex", -] - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "chrono" -version = "0.4.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" -dependencies = [ - "iana-time-zone", - "num-traits", - "serde", - "windows-link", -] - -[[package]] -name = "colorchoice" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" - -[[package]] -name = "commit-chain" -version = "0.4.0" -dependencies = [ - "alloy-primitives", - "base64 0.21.7", - "bincode", - "bitcoin", - "guest-executor", - "hex", - "prost-build", - "revm-database-interface", - "serde", - "sha2 0.10.9", - "tendermint", - "tendermint-light-client-verifier", - "tracing", - "verifier", - "zkm-zkvm", -] - -[[package]] -name = "const-hex" -version = "1.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20d9a563d167a9cce0f94153382b33cb6eded6dfabff03c69ad65a28ea1514e0" -dependencies = [ - "cfg-if", - "cpufeatures", - "proptest", - "serde_core", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const_format" -version = "0.2.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" -dependencies = [ - "const_format_proc_macros", - "konst", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "constant_time_eq" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b" - -[[package]] -name = "convert_case" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "cosmos-sdk-proto" -version = "0.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95ac39be7373404accccaede7cc1ec942ccef14f0ca18d209967a756bf1dbb1f" -dependencies = [ - "informalsystems-pbjson", - "prost", - "serde", - "tendermint-proto 0.40.4", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crc" -version = "3.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" -dependencies = [ - "crc-catalog", -] - -[[package]] -name = "crc-catalog" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" - -[[package]] -name = "critical-section" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b" - -[[package]] -name = "crossbeam-deque" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" -dependencies = [ - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crunchy" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array 0.14.9", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array 0.14.9", - "typenum", -] - -[[package]] -name = "curve25519-dalek" -version = "4.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" -dependencies = [ - "cfg-if", - "cpufeatures", - "curve25519-dalek-derive", - "fiat-crypto", - "rustc_version 0.4.1", - "subtle", - "zeroize", -] - -[[package]] -name = "curve25519-dalek-derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "darling" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cdf337090841a411e2a7f3deb9187445851f91b309c0c0a29e05f74a00a48c0" -dependencies = [ - "darling_core 0.21.3", - "darling_macro 0.21.3", -] - -[[package]] -name = "darling" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" -dependencies = [ - "darling_core 0.23.0", - "darling_macro 0.23.0", -] - -[[package]] -name = "darling_core" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1247195ecd7e3c85f83c8d2a366e4210d588e802133e1e355180a9870b517ea4" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "serde", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_core" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" -dependencies = [ - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d38308df82d1080de0afee5d069fa14b0326a88c14f15c5ccda35b4a6c414c81" -dependencies = [ - "darling_core 0.21.3", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "darling_macro" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" -dependencies = [ - "darling_core 0.23.0", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dashu" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85b3e5ac1e23ff1995ef05b912e2b012a8784506987a2651552db2c73fb3d7e0" -dependencies = [ - "dashu-base", - "dashu-float", - "dashu-int", - "dashu-macros", - "dashu-ratio", - "rustversion", -] - -[[package]] -name = "dashu-base" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b80bf6b85aa68c58ffea2ddb040109943049ce3fbdf4385d0380aef08ef289" - -[[package]] -name = "dashu-float" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85078445a8dbd2e1bd21f04a816f352db8d333643f0c9b78ca7c3d1df71063e7" -dependencies = [ - "dashu-base", - "dashu-int", - "num-modular", - "num-order", - "rustversion", - "static_assertions", -] - -[[package]] -name = "dashu-int" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee99d08031ca34a4d044efbbb21dff9b8c54bb9d8c82a189187c0651ffdb9fbf" -dependencies = [ - "cfg-if", - "dashu-base", - "num-modular", - "num-order", - "rustversion", - "static_assertions", -] - -[[package]] -name = "dashu-macros" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93381c3ef6366766f6e9ed9cf09e4ef9dec69499baf04f0c60e70d653cf0ab10" -dependencies = [ - "dashu-base", - "dashu-float", - "dashu-int", - "dashu-ratio", - "paste", - "proc-macro2", - "quote", - "rustversion", -] - -[[package]] -name = "dashu-ratio" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47e33b04dd7ce1ccf8a02a69d3419e354f2bbfdf4eb911a0b7465487248764c9" -dependencies = [ - "dashu-base", - "dashu-float", - "dashu-int", - "num-modular", - "num-order", - "rustversion", -] - -[[package]] -name = "der" -version = "0.7.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" -dependencies = [ - "const-oid", - "pem-rfc7468", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" -dependencies = [ - "powerfmt", - "serde_core", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "derive-where" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08b3a0bcc0d079199cd476b2cae8435016ec11d1c0986c6901c5ac223041534" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "0.99.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6edb4b64a43d977b8e99788fe3a04d483834fba1215a7e02caa415b626497f7f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_more" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" -dependencies = [ - "derive_more-impl 1.0.0", -] - -[[package]] -name = "derive_more" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" -dependencies = [ - "derive_more-impl 2.1.1", -] - -[[package]] -name = "derive_more-impl" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "derive_more-impl" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version 0.4.1", - "syn 2.0.117", - "unicode-xid", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array 0.14.9", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "dunce" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" - -[[package]] -name = "dyn-clone" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "git+https://github.com/ziren-patches/signatures.git?branch=patch-ecdsa-0.16.9#da0cd8753b243e09acf82f8e58f8da0e28e7287d" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "serdect", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-consensus" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" -dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "sha2 0.9.9", - "zeroize", -] - -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" -dependencies = [ - "serde", -] - -[[package]] -name = "elf" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4445909572dbd556c457c849c4ca58623d84b27c8fff1e74b0b4227d8b90d17b" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff 0.13.1", - "generic-array 0.14.9", - "group 0.13.0", - "hkdf", - "pem-rfc7468", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "enum-map" -version = "2.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6866f3bfdf8207509a033af1a75a7b08abda06bbaaeae6669323fd5a097df2e9" -dependencies = [ - "enum-map-derive", - "serde", -] - -[[package]] -name = "enum-map-derive" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f282cfdfe92516eb26c2af8589c274c7c17681f5ecc03c18255fe741c6aa64eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "enum-ordinalize" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1091a7bb1f8f2c4b28f1fe2cef4980ca2d410a3d727d67ecc3178c9b0800f0" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ca9601fb2d62598ee17836250842873a413586e5d7ed88b356e38ddbb0ec631" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "env_filter" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32e90c2accc4b07a8456ea0debdc2e7587bdd890680d71173a15d4ae604f6eef" -dependencies = [ - "log", - "regex", -] - -[[package]] -name = "env_logger" -version = "0.11.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0621c04f2196ac3f488dd583365b9c09be011a4ab8b9f37248ffcc8f6198b56a" -dependencies = [ - "anstream", - "anstyle", - "env_filter", - "jiff", - "log", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" -dependencies = [ - "libc", - "windows-sys", -] - -[[package]] -name = "eyre" -version = "0.6.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" -dependencies = [ - "indenter", - "once_cell", -] - -[[package]] -name = "fastrand" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" - -[[package]] -name = "fastrlp" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139834ddba373bbdd213dffe02c8d110508dcf1726c2be27e8d1f7d7e1856418" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "fastrlp" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8dba4714ef14b8274c371879b175aa55b16b30f269663f19d576f380018dc4" -dependencies = [ - "arrayvec", - "auto_impl", - "bytes", -] - -[[package]] -name = "ff" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" -dependencies = [ - "bitvec", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "bitvec", - "byteorder", - "ff_derive", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "ff_derive" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f10d12652036b0e99197587c6ba87a8fc3031986499973c030d8b44fcc151b60" -dependencies = [ - "addchain", - "num-bigint 0.3.3", - "num-integer", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "fiat-crypto" -version = "0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixed-hash" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" -dependencies = [ - "byteorder", - "rand 0.8.6", - "rustc-hex", - "static_assertions", -] - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "flex-error" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" -dependencies = [ - "paste", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "foldhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb" - -[[package]] -name = "form_urlencoded" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b147ee9d1f6d097cef9ce628cd2ee62288d963e16fb287bd9286455b241382d" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-io" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718" - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-sink", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "futures-utils-wasm" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42012b0f064e01aa58b545fe3727f90f7dd4020f4a3ea735b50344965f5a57e9" - -[[package]] -name = "gcd" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d758ba1b47b00caf47f24925c0074ecb20d6dfcffe7f6d53395c0465674841a" - -[[package]] -name = "generic-array" -version = "0.14.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "generic-array" -version = "1.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dab9e9188e97a93276e1fe7b56401b851e2b45a46d045ca658100c1303ada649" -dependencies = [ - "rustversion", - "serde_core", - "typenum", -] - -[[package]] -name = "getrandom" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "libc", - "r-efi 5.3.0", - "wasip2", -] - -[[package]] -name = "getrandom" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", - "wasip2", - "wasip3", -] - -[[package]] -name = "gimli" -version = "0.32.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" - -[[package]] -name = "glob" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280" - -[[package]] -name = "group" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" -dependencies = [ - "ff 0.12.1", - "memuse", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff 0.13.1", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "guest" -version = "1.1.0" -dependencies = [ - "bitcoin", - "bitcoin-light-client-circuit", - "hex", - "tracing", - "verifier", - "zkm-primitives", - "zkm-zkvm", -] - -[[package]] -name = "guest-executor" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" -dependencies = [ - "alloy-consensus", - "alloy-evm", - "alloy-network", - "alloy-primitives", - "alloy-rpc-types", - "bincode", - "itertools 0.13.0", - "kzg-rs", - "mpt", - "primitives", - "reth-chainspec", - "reth-consensus", - "reth-consensus-common", - "reth-errors", - "reth-ethereum-consensus", - "reth-ethereum-primitives", - "reth-evm", - "reth-evm-ethereum", - "reth-execution-types", - "reth-primitives-traits", - "reth-trie", - "revm", - "revm-primitives", - "serde", - "serde_json", - "serde_with", - "thiserror 1.0.69", -] - -[[package]] -name = "halo2" -version = "0.1.0-beta.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a23c779b38253fe1538102da44ad5bd5378495a61d2c4ee18d64eaa61ae5995" -dependencies = [ - "halo2_proofs", -] - -[[package]] -name = "halo2_proofs" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e925780549adee8364c7f2b685c753f6f3df23bde520c67416e93bf615933760" -dependencies = [ - "blake2b_simd", - "ff 0.12.1", - "group 0.12.1", - "pasta_curves 0.4.1", - "rand_core 0.6.4", - "rayon", -] - -[[package]] -name = "halo2curves" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d380afeef3f1d4d3245b76895172018cfb087d9976a7cabcd5597775b2933e07" -dependencies = [ - "blake2", - "digest 0.10.7", - "ff 0.13.1", - "group 0.13.0", - "halo2derive", - "hex", - "lazy_static", - "num-bigint 0.4.6", - "num-integer", - "num-traits", - "pairing 0.23.0", - "pasta_curves 0.5.1", - "paste", - "rand 0.8.6", - "rand_core 0.6.4", - "rayon", - "serde", - "serde_arrays 0.1.0", - "sha2 0.10.9", - "static_assertions", - "subtle", - "unroll", -] - -[[package]] -name = "halo2derive" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdb99e7492b4f5ff469d238db464131b86c2eaac814a78715acba369f64d2c76" -dependencies = [ - "num-bigint 0.4.6", - "num-integer", - "num-traits", - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "hashbrown" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "ahash", - "allocator-api2", - "serde", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "allocator-api2", - "foldhash 0.1.5", -] - -[[package]] -name = "hashbrown" -version = "0.16.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" -dependencies = [ - "foldhash 0.2.0", - "serde", - "serde_core", -] - -[[package]] -name = "hashbrown" -version = "0.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" - -[[package]] -name = "header-chain" -version = "0.4.0" -dependencies = [ - "bitcoin", - "borsh", - "crypto-bigint", - "serde", - "sha2 0.10.9", - "verifier", - "zkm-zkvm", -] - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hermit-abi" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" -dependencies = [ - "serde", -] - -[[package]] -name = "hex-conservative" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fda06d18ac606267c40c04e41b9947729bf8b9efe74bd4e82b61a5f26a510b9f" -dependencies = [ - "arrayvec", -] - -[[package]] -name = "hex_lit" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - -[[package]] -name = "icu_collections" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" -dependencies = [ - "displaydoc", - "potential_utf", - "utf8_iter", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locale_core" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_normalizer" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" -dependencies = [ - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" - -[[package]] -name = "icu_properties" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" -dependencies = [ - "icu_collections", - "icu_locale_core", - "icu_properties_data", - "icu_provider", - "zerotrie", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" - -[[package]] -name = "icu_provider" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" -dependencies = [ - "displaydoc", - "icu_locale_core", - "writeable", - "yoke", - "zerofrom", - "zerotrie", - "zerovec", -] - -[[package]] -name = "id-arena" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954" - -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - -[[package]] -name = "idna" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "impl-codec" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" -dependencies = [ - "parity-scale-codec", -] - -[[package]] -name = "impl-trait-for-tuples" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0eb5a3343abf848c0984fe4604b2b105da9539376e24fc0a3b0007411ae4fd9" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "indenter" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" - -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.0", - "serde", - "serde_core", -] - -[[package]] -name = "informalsystems-pbjson" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aa4a0980c8379295100d70854354e78df2ee1c6ca0f96ffe89afeb3140e3a3d" -dependencies = [ - "base64 0.21.7", - "serde", -] - -[[package]] -name = "is_terminal_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" - -[[package]] -name = "itertools" -version = "0.10.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "jiff" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" -dependencies = [ - "jiff-static", - "log", - "portable-atomic", - "portable-atomic-util", - "serde_core", -] - -[[package]] -name = "jiff-static" -version = "0.2.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "jobserver" -version = "0.1.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33" -dependencies = [ - "getrandom 0.3.4", - "libc", -] - -[[package]] -name = "js-sys" -version = "0.3.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" -dependencies = [ - "cfg-if", - "futures-util", - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jubjub" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a575df5f985fe1cd5b2b05664ff6accfc46559032b954529fd225a2168d27b0f" -dependencies = [ - "bitvec", - "bls12_381 0.7.1", - "ff 0.12.1", - "group 0.12.1", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-k256-0.13.4#8266b228a39402a0ba68d644b7f26b85b5112fe3" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "hex", - "once_cell", - "serdect", - "sha2 0.10.9", - "signature", - "zkm-lib", -] - -[[package]] -name = "keccak" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb26cec98cce3a3d96cbb7bced3c4b16e3d13f27ec56dbd62cbc8f39cfb9d653" -dependencies = [ - "cpufeatures", -] - -[[package]] -name = "keccak-asm" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa468878266ad91431012b3e5ef1bf9b170eab22883503a318d46857afa4579a" -dependencies = [ - "digest 0.10.7", - "sha3-asm", -] - -[[package]] -name = "konst" -version = "0.2.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" -dependencies = [ - "konst_macro_rules", -] - -[[package]] -name = "konst_macro_rules" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" - -[[package]] -name = "kzg-rs" -version = "0.2.6" -source = "git+https://github.com/ziren-patches/kzg-rs?branch=ziren#4cd5e3086a49dc1e94ffbd63ee4a7a9b2e342faa" -dependencies = [ - "bls12_381 0.8.0", - "ff 0.13.1", - "hex", - "serde_arrays 0.2.0", - "sha2 0.10.9", - "spin", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" -dependencies = [ - "spin", -] - -[[package]] -name = "leb128fmt" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "libm" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" - -[[package]] -name = "linux-raw-sys" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" - -[[package]] -name = "litemap" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" - -[[package]] -name = "lock_api" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" - -[[package]] -name = "macro-string" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b27834086c65ec3f9387b096d66e99f221cf081c2b738042aa252bcd41204e3" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "matchers" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" -dependencies = [ - "regex-automata", -] - -[[package]] -name = "memchr" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" - -[[package]] -name = "memuse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d97bbf43eb4f088f8ca469930cde17fa036207c9a5e02ccc5107c4e8b17c964" - -[[package]] -name = "miniz_oxide" -version = "0.8.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" -dependencies = [ - "adler2", -] - -[[package]] -name = "modular-bitfield" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a53d79ba8304ac1c4f9eb3b9d281f21f7be9d4626f72ce7df4ad8fbde4f38a74" -dependencies = [ - "modular-bitfield-impl", - "static_assertions", -] - -[[package]] -name = "modular-bitfield-impl" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a7d5f7076603ebc68de2dc6a650ec331a062a13abaa346975be747bbfa4b789" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "mpt" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types", - "alloy-rpc-types-debug", - "reth-trie", - "rlp", - "serde", - "thiserror 1.0.69", -] - -[[package]] -name = "multimap" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "ntapi" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae" -dependencies = [ - "winapi", -] - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" -dependencies = [ - "num-bigint 0.4.6", - "num-complex", - "num-integer", - "num-iter", - "num-rational", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6f7833f2cbf2360a6cfd58cd41a53aa7a90bd4c202f5b1c7dd2ed73c57b2c3" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", - "rand 0.8.6", -] - -[[package]] -name = "num-complex" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-iter" -version = "0.1.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" -dependencies = [ - "autocfg", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-modular" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17bb261bf36fa7d83f4c294f834e91256769097b3cb505d44831e0a179ac647f" - -[[package]] -name = "num-order" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537b596b97c40fcf8056d153049eb22f481c17ebce72a513ec9286e4986d1bb6" -dependencies = [ - "num-modular", -] - -[[package]] -name = "num-rational" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" -dependencies = [ - "num-bigint 0.4.6", - "num-integer", - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", - "libm", -] - -[[package]] -name = "num_cpus" -version = "1.17.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "num_enum" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d0bca838442ec211fa11de3a8b0e0e8f3a4522575b5c4c06ed722e005036f26" -dependencies = [ - "num_enum_derive", - "rustversion", -] - -[[package]] -name = "num_enum_derive" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "nums" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf3c74f925fb8cfc49a8022f2afce48a0683b70f9e439885594e84c5edbf5b01" -dependencies = [ - "num-bigint 0.4.6", - "num-integer", - "num-traits", - "rand 0.8.6", -] - -[[package]] -name = "nybbles" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d49ff0c0d00d4a502b39df9af3a525e1efeb14b9dabb5bb83335284c1309210" -dependencies = [ - "alloy-rlp", - "cfg-if", - "proptest", - "ruint", - "serde", - "smallvec", -] - -[[package]] -name = "object" -version = "0.37.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe" -dependencies = [ - "memchr", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" -dependencies = [ - "critical-section", - "portable-atomic", -] - -[[package]] -name = "once_cell_polyfill" -version = "1.70.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" - -[[package]] -name = "op-alloy-consensus" -version = "0.22.0" -source = "git+https://github.com/ziren-patches/op-alloy?branch=patch-0.22.0#20048eaa105b1c06ca4fca6108a047a2fda0c494" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-serde", - "derive_more 2.1.1", - "serde", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -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" -dependencies = [ - "ecdsa", - "elliptic-curve", - "hex", - "primeorder", - "sha2 0.10.9", - "zkm-lib", -] - -[[package]] -name = "p3-air" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "p3-field", - "p3-matrix", -] - -[[package]] -name = "p3-bn254-fr" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "ff 0.13.1", - "halo2curves", - "num-bigint 0.4.6", - "p3-field", - "p3-poseidon2", - "p3-symmetric", - "rand 0.8.6", - "serde", -] - -[[package]] -name = "p3-challenger" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "p3-field", - "p3-maybe-rayon", - "p3-symmetric", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-circle" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-fri", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-commit" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-challenger", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-util", - "serde", -] - -[[package]] -name = "p3-dft" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "tracing", -] - -[[package]] -name = "p3-field" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "num-bigint 0.4.6", - "num-integer", - "num-traits", - "nums", - "p3-maybe-rayon", - "p3-util", - "rand 0.8.6", - "serde", - "tracing", -] - -[[package]] -name = "p3-fri" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-interpolation", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "rand 0.8.6", - "serde", - "tracing", -] - -[[package]] -name = "p3-interpolation" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", -] - -[[package]] -name = "p3-keccak" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-field", - "p3-symmetric", - "p3-util", - "tiny-keccak", -] - -[[package]] -name = "p3-keccak-air" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "p3-air", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "tracing", -] - -[[package]] -name = "p3-koala-bear" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "p3-field", - "p3-mds", - "p3-monty-31", - "p3-poseidon2", - "p3-symmetric", - "rand 0.8.6", - "serde", -] - -[[package]] -name = "p3-matrix" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-field", - "p3-maybe-rayon", - "p3-util", - "rand 0.8.6", - "serde", - "tracing", - "transpose", -] - -[[package]] -name = "p3-maybe-rayon" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "rayon", -] - -[[package]] -name = "p3-mds" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-symmetric", - "p3-util", - "rand 0.8.6", -] - -[[package]] -name = "p3-merkle-tree" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-commit", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-symmetric", - "p3-util", - "rand 0.8.6", - "serde", - "tracing", -] - -[[package]] -name = "p3-mersenne-31" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "num-bigint 0.4.6", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "p3-util", - "rand 0.8.6", - "serde", -] - -[[package]] -name = "p3-monty-31" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "num-bigint 0.4.6", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-mds", - "p3-poseidon2", - "p3-symmetric", - "p3-util", - "rand 0.8.6", - "serde", - "serde_json", - "tracing", - "transpose", -] - -[[package]] -name = "p3-poseidon2" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "gcd", - "p3-field", - "p3-mds", - "p3-symmetric", - "rand 0.8.6", - "serde", -] - -[[package]] -name = "p3-symmetric" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-field", - "serde", -] - -[[package]] -name = "p3-uni-stark" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "itertools 0.13.0", - "p3-air", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-matrix", - "p3-maybe-rayon", - "p3-util", - "serde", - "tracing", -] - -[[package]] -name = "p3-util" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/Plonky3#faa24ca4597eebeecbf71b194b71c7d1a99b3f01" -dependencies = [ - "lock_api", - "serde", -] - -[[package]] -name = "pairing" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135590d8bdba2b31346f9cd1fb2a912329f5135e832a4f422942eb6ead8b6b3b" -dependencies = [ - "group 0.12.1", -] - -[[package]] -name = "pairing" -version = "0.23.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fec4625e73cf41ef4bb6846cafa6d44736525f442ba45e407c4a000a13996f" -dependencies = [ - "group 0.13.0", -] - -[[package]] -name = "parity-scale-codec" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "799781ae679d79a948e13d4824a40970bfa500058d245760dd857301059810fa" -dependencies = [ - "arrayvec", - "bitvec", - "byte-slice-cast", - "const_format", - "impl-trait-for-tuples", - "parity-scale-codec-derive", - "rustversion", - "serde", -] - -[[package]] -name = "parity-scale-codec-derive" -version = "3.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b4653168b563151153c9e4c08ebed57fb8262bebfa79711552fa983c623e7a" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "pasta_curves" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cc65faf8e7313b4b1fbaa9f7ca917a0eed499a9663be71477f87993604341d8" -dependencies = [ - "blake2b_simd", - "ff 0.12.1", - "group 0.12.1", - "lazy_static", - "rand 0.8.6", - "static_assertions", - "subtle", -] - -[[package]] -name = "pasta_curves" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e57598f73cc7e1b2ac63c79c517b31a0877cd7c402cdcaa311b5208de7a095" -dependencies = [ - "blake2b_simd", - "ff 0.13.1", - "group 0.13.0", - "hex", - "lazy_static", - "rand 0.8.6", - "serde", - "static_assertions", - "subtle", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "pem-rfc7468" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" -dependencies = [ - "base64ct", -] - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "petgraph" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3672b37090dbd86368a4145bc067582552b29c27377cad4e0a306c97f9bd7772" -dependencies = [ - "fixedbitset", - "indexmap 2.14.0", -] - -[[package]] -name = "phf" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "phf_shared" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "portable-atomic" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" - -[[package]] -name = "portable-atomic-util" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" -dependencies = [ - "portable-atomic", -] - -[[package]] -name = "potential_utf" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" -dependencies = [ - "zerovec", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.37" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" -dependencies = [ - "proc-macro2", - "syn 2.0.117", -] - -[[package]] -name = "primeorder" -version = "0.13.1" -source = "git+https://github.com/ziren-patches/elliptic-curves?branch=patch-p256-0.13.2#a6f1a1fb07020d00f627725a20dc336983be3946" -dependencies = [ - "elliptic-curve", -] - -[[package]] -name = "primitive-types" -version = "0.12.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b34d9fd68ae0b74a41b21c03c2f62847aa0ffea044eee893b4c140b37e244e2" -dependencies = [ - "fixed-hash", - "impl-codec", - "uint", -] - -[[package]] -name = "primitives" -version = "0.1.0" -source = "git+https://github.com/ProjectZKM/reth-processor?branch=feat%2Fslot-state-check#294aa06127be4bfda47a7fd4905b93bb8e664dca" -dependencies = [ - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-rpc-types", - "eyre", - "reth-chainspec", - "reth-primitives-traits", - "reth-trie", - "serde", - "serde_json", -] - -[[package]] -name = "proc-macro-crate" -version = "3.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro-error-attr2" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" -dependencies = [ - "proc-macro2", - "quote", -] - -[[package]] -name = "proc-macro-error2" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" -dependencies = [ - "proc-macro-error-attr2", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "proptest" -version = "1.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" -dependencies = [ - "bit-set", - "bit-vec", - "bitflags", - "num-traits", - "rand 0.9.4", - "rand_chacha 0.9.0", - "rand_xorshift", - "regex-syntax", - "rusty-fork", - "tempfile", - "unarray", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck", - "itertools 0.14.0", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn 2.0.117", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools 0.14.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost", -] - -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - -[[package]] -name = "quote" -version = "1.0.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", - "serde", -] - -[[package]] -name = "rand" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", - "serde", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.17", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", - "serde", -] - -[[package]] -name = "rand_xorshift" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" -dependencies = [ - "rand_core 0.9.5", -] - -[[package]] -name = "rayon" -version = "1.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" -dependencies = [ - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91" -dependencies = [ - "crossbeam-deque", - "crossbeam-utils", -] - -[[package]] -name = "rayon-scan" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f87cc11a0140b4b0da0ffc889885760c61b13672d80a908920b2c0df078fa14" -dependencies = [ - "rayon", -] - -[[package]] -name = "ref-cast" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d" -dependencies = [ - "ref-cast-impl", -] - -[[package]] -name = "ref-cast-impl" -version = "1.0.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "regex" -version = "1.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" - -[[package]] -name = "reth-chainspec" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-chains", - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-genesis", - "alloy-primitives", - "alloy-trie", - "auto_impl", - "derive_more 2.1.1", - "reth-ethereum-forks", - "reth-network-peers", - "reth-primitives-traits", - "serde_json", -] - -[[package]] -name = "reth-codecs" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-trie", - "bytes", - "modular-bitfield", - "op-alloy-consensus", - "reth-codecs-derive", - "reth-zstd-compressors", - "serde", -] - -[[package]] -name = "reth-codecs-derive" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "reth-consensus" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "auto_impl", - "reth-execution-types", - "reth-primitives-traits", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-consensus-common" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "reth-chainspec", - "reth-consensus", - "reth-primitives-traits", -] - -[[package]] -name = "reth-db-models" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "reth-primitives-traits", -] - -[[package]] -name = "reth-errors" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "reth-consensus", - "reth-execution-errors", - "reth-storage-errors", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-ethereum-consensus" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "reth-chainspec", - "reth-consensus", - "reth-consensus-common", - "reth-execution-types", - "reth-primitives-traits", - "tracing", -] - -[[package]] -name = "reth-ethereum-forks" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-eip2124", - "alloy-hardforks", - "alloy-primitives", - "auto_impl", - "once_cell", -] - -[[package]] -name = "reth-ethereum-primitives" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-serde", - "reth-codecs", - "reth-primitives-traits", - "serde", - "serde_with", -] - -[[package]] -name = "reth-evm" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "anyhow", - "auto_impl", - "derive_more 2.1.1", - "futures-util", - "reth-execution-errors", - "reth-execution-types", - "reth-primitives-traits", - "reth-storage-api", - "reth-storage-errors", - "reth-trie-common", - "revm", - "revm-database", -] - -[[package]] -name = "reth-evm-ethereum" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "alloy-rpc-types-engine", - "reth-chainspec", - "reth-ethereum-forks", - "reth-ethereum-primitives", - "reth-evm", - "reth-execution-types", - "reth-primitives-traits", - "reth-storage-errors", - "revm", -] - -[[package]] -name = "reth-execution-errors" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-evm", - "alloy-primitives", - "alloy-rlp", - "nybbles", - "reth-storage-errors", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-execution-types" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-evm", - "alloy-primitives", - "derive_more 2.1.1", - "reth-ethereum-primitives", - "reth-primitives-traits", - "reth-trie-common", - "revm", -] - -[[package]] -name = "reth-network-peers" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "serde_with", - "thiserror 2.0.18", - "url", -] - -[[package]] -name = "reth-primitives-traits" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-genesis", - "alloy-primitives", - "alloy-rlp", - "alloy-rpc-types-eth", - "alloy-trie", - "auto_impl", - "bytes", - "derive_more 2.1.1", - "once_cell", - "op-alloy-consensus", - "reth-codecs", - "revm-bytecode", - "revm-primitives", - "revm-state", - "secp256k1 0.30.0", - "serde", - "serde_with", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-prune-types" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-primitives", - "derive_more 2.1.1", - "strum 0.27.2", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-stages-types" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-primitives", - "reth-trie-common", -] - -[[package]] -name = "reth-static-file-types" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-primitives", - "derive_more 2.1.1", - "serde", - "strum 0.27.2", -] - -[[package]] -name = "reth-storage-api" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rpc-types-engine", - "auto_impl", - "reth-chainspec", - "reth-db-models", - "reth-ethereum-primitives", - "reth-execution-types", - "reth-primitives-traits", - "reth-prune-types", - "reth-stages-types", - "reth-storage-errors", - "reth-trie-common", - "revm-database", -] - -[[package]] -name = "reth-storage-errors" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "derive_more 2.1.1", - "reth-primitives-traits", - "reth-prune-types", - "reth-static-file-types", - "revm-database-interface", - "thiserror 2.0.18", -] - -[[package]] -name = "reth-trie" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-eips", - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "auto_impl", - "itertools 0.14.0", - "reth-execution-errors", - "reth-primitives-traits", - "reth-stages-types", - "reth-storage-errors", - "reth-trie-common", - "reth-trie-sparse", - "revm-database", - "tracing", -] - -[[package]] -name = "reth-trie-common" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "derive_more 2.1.1", - "itertools 0.14.0", - "nybbles", - "rayon", - "reth-primitives-traits", - "revm-database", -] - -[[package]] -name = "reth-trie-sparse" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "alloy-primitives", - "alloy-rlp", - "alloy-trie", - "auto_impl", - "reth-execution-errors", - "reth-primitives-traits", - "reth-trie-common", - "smallvec", - "tracing", -] - -[[package]] -name = "reth-zstd-compressors" -version = "1.9.3" -source = "git+https://github.com/ziren-patches/reth?branch=patch-1.9.3#fb7e5b4b347ac28d7f3f937698a3b084bd158aa2" -dependencies = [ - "zstd", -] - -[[package]] -name = "revm" -version = "31.0.2" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database", - "revm-database-interface", - "revm-handler", - "revm-inspector", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", -] - -[[package]] -name = "revm-bytecode" -version = "7.1.1" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "bitvec", - "phf", - "revm-primitives", - "serde", -] - -[[package]] -name = "revm-context" -version = "11.0.2" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "alloy-consensus", - "bitvec", - "cfg-if", - "derive-where", - "revm-bytecode", - "revm-context-interface", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-context-interface" -version = "12.0.1" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "alloy-consensus", - "alloy-eip2930", - "alloy-eip7702", - "auto_impl", - "either", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-database" -version = "9.0.5" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "alloy-eips", - "revm-bytecode", - "revm-database-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-database-interface" -version = "8.0.5" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "auto_impl", - "either", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-handler" -version = "12.0.2" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "auto_impl", - "derive-where", - "revm-bytecode", - "revm-context", - "revm-context-interface", - "revm-database-interface", - "revm-interpreter", - "revm-precompile", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-inspector" -version = "12.0.2" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "auto_impl", - "either", - "revm-context", - "revm-database-interface", - "revm-handler", - "revm-interpreter", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-interpreter" -version = "29.0.1" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "revm-bytecode", - "revm-context-interface", - "revm-primitives", - "revm-state", - "serde", -] - -[[package]] -name = "revm-precompile" -version = "29.0.1" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "ark-bls12-381", - "ark-bn254", - "ark-ec", - "ark-ff 0.5.0", - "ark-serialize 0.5.0", - "arrayref", - "aurora-engine-modexp", - "cfg-if", - "k256", - "p256", - "revm-primitives", - "ripemd", - "sha2 0.10.9", - "substrate-bn", -] - -[[package]] -name = "revm-primitives" -version = "21.0.2" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "alloy-primitives", - "num_enum", - "once_cell", - "serde", -] - -[[package]] -name = "revm-state" -version = "8.1.1" -source = "git+https://github.com/ziren-patches/revm?branch=patch-31.0.2#0cd8a9cffabcc01fd92b8f12d6d601040e0ec88f" -dependencies = [ - "bitflags", - "revm-bytecode", - "revm-primitives", - "serde", -] - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "git+https://github.com/ziren-patches/signatures.git?branch=patch-ecdsa-0.16.9#da0cd8753b243e09acf82f8e58f8da0e28e7287d" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "rlp" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" -dependencies = [ - "bytes", - "rustc-hex", -] - -[[package]] -name = "ruint" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" -dependencies = [ - "alloy-rlp", - "ark-ff 0.3.0", - "ark-ff 0.4.2", - "ark-ff 0.5.0", - "bytes", - "fastrlp 0.3.1", - "fastrlp 0.4.0", - "num-bigint 0.4.6", - "num-integer", - "num-traits", - "parity-scale-codec", - "primitive-types", - "proptest", - "rand 0.8.6", - "rand 0.9.4", - "rlp", - "ruint-macro", - "serde_core", - "valuable", - "zeroize", -] - -[[package]] -name = "ruint-macro" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" - -[[package]] -name = "rustc-demangle" -version = "0.1.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50b8869d9fc858ce7266cce0194bd74df58b9d0e3f6df3a9fc8eb470d95c09d" - -[[package]] -name = "rustc-hash" -version = "2.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe" - -[[package]] -name = "rustc-hex" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" - -[[package]] -name = "rustc_version" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0dfe2087c51c460008730de8b57e6a320782fbfb312e1f4d520e6c6fae155ee" -dependencies = [ - "semver 0.11.0", -] - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver 1.0.28", -] - -[[package]] -name = "rustix" -version = "1.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - -[[package]] -name = "rustversion" -version = "1.0.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" - -[[package]] -name = "rusty-fork" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" -dependencies = [ - "fnv", - "quick-error", - "tempfile", - "wait-timeout", -] - -[[package]] -name = "scale-info" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "346a3b32eba2640d17a9cb5927056b08f3de90f65b72fe09402c2ad07d684d0b" -dependencies = [ - "cfg-if", - "derive_more 1.0.0", - "parity-scale-codec", - "scale-info-derive", -] - -[[package]] -name = "scale-info-derive" -version = "2.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6630024bf739e2179b91fb424b28898baf819414262c5d376677dbff1fe7ebf" -dependencies = [ - "proc-macro-crate", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "schemars" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cd191f9397d57d581cddd31014772520aa448f65ef991055d7f61582c65165f" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "schemars" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2b42f36aa1cd011945615b92222f6bf73c599a102a300334cd7f8dbeec726cc" -dependencies = [ - "dyn-clone", - "ref-cast", - "serde", - "serde_json", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array 0.14.9", - "pkcs8", - "serdect", - "subtle", - "zeroize", -] - -[[package]] -name = "secp256k1" -version = "0.29.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e0cc0f1cf93f4969faf3ea1c7d8a9faed25918d96affa959720823dfe86d4f3" -dependencies = [ - "bitcoin_hashes", - "rand 0.8.6", - "secp256k1-sys", - "serde", -] - -[[package]] -name = "secp256k1" -version = "0.30.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" -dependencies = [ - "bitcoin_hashes", - "rand 0.8.6", - "secp256k1-sys", - "serde", -] - -[[package]] -name = "secp256k1-sys" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4387882333d3aa8cb20530a17c69a3752e97837832f34f6dccc760e715001d9" -dependencies = [ - "cc", -] - -[[package]] -name = "semver" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" - -[[package]] -name = "semver-parser" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9900206b54a3527fdc7b8a938bffd94a568bac4f4aa8113b209df75a09c0dec2" -dependencies = [ - "pest", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_arrays" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38636132857f68ec3d5f3eb121166d2af33cb55174c4d5ff645db6165cbef0fd" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_arrays" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94a16b99c5ea4fe3daccd14853ad260ec00ea043b2708d1fd1da3106dcd8d9df" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_bytes" -version = "0.11.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" -dependencies = [ - "serde", - "serde_core", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serde_with" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f05839ce67618e14a09b286535c0d9c94e85ef25469b0e13cb4f844e5593eb19" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.14.0", - "schemars 0.9.0", - "schemars 1.2.1", - "serde_core", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf2ebbe86054f9b45bc3881e865683ccfaccce97b9b4cb53f3039d67f355a334" -dependencies = [ - "darling 0.23.0", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "serdect" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" -dependencies = [ - "base16ct", - "serde", -] - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha2" -version = "0.10.9" -source = "git+https://github.com/ziren-patches/RustCrypto-hashes?branch=patch-sha2-0.10.9#dbfdbd088ac7b55e03c0b9eb192278efe35d67dc" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha3" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" -dependencies = [ - "digest 0.10.7", - "keccak", -] - -[[package]] -name = "sha3-asm" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59cbb88c189d6352cc8ae96a39d19c7ecad8f7330b29461187f2587fdc2988d5" -dependencies = [ - "cc", - "cfg-if", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - -[[package]] -name = "size" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fed904c7fb2856d868b92464fc8fa597fce366edea1a9cbfaa8cb5fe080bd6d" - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" -dependencies = [ - "serde", -] - -[[package]] -name = "snowbridge-amcl" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "460a9ed63cdf03c1b9847e8a12a5f5ba19c4efd5869e4a737e05be25d7c427e5" -dependencies = [ - "parity-scale-codec", - "scale-info", -] - -[[package]] -name = "spin" -version = "0.9.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" - -[[package]] -name = "state-chain" -version = "0.4.0" -dependencies = [ - "alloy-consensus", - "alloy-primitives", - "base64 0.21.7", - "bincode", - "bitcoin", - "cosmos-sdk-proto", - "guest-executor", - "header-chain", - "hex", - "prost", - "prost-build", - "prost-derive", - "revm-database-interface", - "serde", - "serde_json", - "sha2 0.10.9", - "tendermint", - "tendermint-light-client-verifier", - "tracing", - "verifier", - "zkm-zkvm", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "strength_reduce" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe895eb47f22e2ddd4dabc02bce419d2e643c8e3b585c78158b349195bc24d82" - -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - -[[package]] -name = "strum" -version = "0.26.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06" -dependencies = [ - "strum_macros 0.26.4", -] - -[[package]] -name = "strum" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" -dependencies = [ - "strum_macros 0.27.2", -] - -[[package]] -name = "strum_macros" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn 2.0.117", -] - -[[package]] -name = "strum_macros" -version = "0.27.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "substrate-bn" -version = "0.6.0" -source = "git+https://github.com/ziren-patches/bn?branch=patch-0.6.0#aba71380457d798039111e6cc0fdf2e0718c6766" -dependencies = [ - "bytemuck", - "byteorder", - "cfg-if", - "crunchy", - "lazy_static", - "num-bigint 0.4.6", - "rand 0.8.6", - "rustc-hex", - "zkm-lib", -] - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "subtle-encoding" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" -dependencies = [ - "zeroize", -] - -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[package]] -name = "syn" -version = "1.0.109" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "2.0.117" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn-solidity" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53f425ae0b12e2f5ae65542e00898d500d4d318b4baf09f40fd0d410454e9947" -dependencies = [ - "paste", - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "synstructure" -version = "0.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "sysinfo" -version = "0.30.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a5b4ddaee55fb2bea2bf0e5000747e5f5c0de765e5a5ff87f4cd106439f4bb3" -dependencies = [ - "cfg-if", - "core-foundation-sys", - "libc", - "ntapi", - "once_cell", - "rayon", - "windows", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.27.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" -dependencies = [ - "fastrand", - "getrandom 0.4.2", - "once_cell", - "rustix", - "windows-sys", -] - -[[package]] -name = "tendermint" -version = "0.40.3" -source = "git+https://github.com/ProjectZKM/tendermint-rs?branch=patch-0.40.3#5bd318a89601f7db2bce05018246c5ef6dc7fb9b" -dependencies = [ - "bytes", - "digest 0.10.7", - "ed25519", - "ed25519-consensus", - "flex-error", - "futures", - "k256", - "num-traits", - "once_cell", - "prost", - "ripemd", - "serde", - "serde_bytes", - "serde_json", - "serde_repr", - "sha2 0.10.9", - "signature", - "subtle", - "subtle-encoding", - "tendermint-proto 0.40.3", - "time", - "zeroize", -] - -[[package]] -name = "tendermint-light-client-verifier" -version = "0.40.3" -source = "git+https://github.com/ProjectZKM/tendermint-rs?branch=patch-0.40.3#5bd318a89601f7db2bce05018246c5ef6dc7fb9b" -dependencies = [ - "derive_more 0.99.20", - "flex-error", - "serde", - "tendermint", - "time", -] - -[[package]] -name = "tendermint-proto" -version = "0.40.3" -source = "git+https://github.com/ProjectZKM/tendermint-rs?branch=patch-0.40.3#5bd318a89601f7db2bce05018246c5ef6dc7fb9b" -dependencies = [ - "bytes", - "flex-error", - "prost", - "serde", - "serde_bytes", - "subtle-encoding", - "time", -] - -[[package]] -name = "tendermint-proto" -version = "0.40.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2c40e13d39ca19082d8a7ed22de7595979350319833698f8b1080f29620a094" -dependencies = [ - "bytes", - "flex-error", - "prost", - "serde", - "serde_bytes", - "subtle-encoding", - "time", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" -dependencies = [ - "thiserror-impl 2.0.18", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "thread_local" -version = "1.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "threadpool" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" -dependencies = [ - "num_cpus", -] - -[[package]] -name = "time" -version = "0.3.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" -dependencies = [ - "deranged", - "itoa", - "num-conv", - "powerfmt", - "serde_core", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" - -[[package]] -name = "time-macros" -version = "0.2.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinystr" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "toml_datetime" -version = "1.1.1+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" -dependencies = [ - "serde_core", -] - -[[package]] -name = "toml_edit" -version = "0.25.11+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" -dependencies = [ - "indexmap 2.14.0", - "toml_datetime", - "toml_parser", - "winnow 1.0.2", -] - -[[package]] -name = "toml_parser" -version = "1.1.2+spec-1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" -dependencies = [ - "winnow 1.0.2", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-forest" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee40835db14ddd1e3ba414292272eddde9dad04d3d4b65509656414d1c42592f" -dependencies = [ - "ansi_term", - "smallvec", - "thiserror 1.0.69", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "matchers", - "nu-ansi-term", - "once_cell", - "regex-automata", - "sharded-slab", - "smallvec", - "thread_local", - "tracing", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "transpose" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad61aed86bc3faea4300c7aee358b4c6d0c8d6ccc36524c96e4c92ccf26e77e" -dependencies = [ - "num-integer", - "strength_reduce", -] - -[[package]] -name = "typenum" -version = "1.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" - -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - -[[package]] -name = "uint" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" -dependencies = [ - "byteorder", - "crunchy", - "hex", - "static_assertions", -] - -[[package]] -name = "unarray" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unroll" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ad948c1cb799b1a70f836077721a92a35ac177d4daddf4c20a633786d4cf618" -dependencies = [ - "quote", - "syn 1.0.109", -] - -[[package]] -name = "url" -version = "2.5.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "utf8parse" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" -dependencies = [ - "serde", -] - -[[package]] -name = "verifier" -version = "0.4.0" -dependencies = [ - "zkm-verifier", -] - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wait-timeout" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" -dependencies = [ - "libc", -] - -[[package]] -name = "wasi" -version = "0.11.1+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" - -[[package]] -name = "wasip2" -version = "1.0.3+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" -dependencies = [ - "wit-bindgen 0.57.1", -] - -[[package]] -name = "wasip3" -version = "0.4.0+wasi-0.3.0-rc-2026-01-06" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" -dependencies = [ - "wit-bindgen 0.51.0", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.117", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.120" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "wasm-encoder" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" -dependencies = [ - "leb128fmt", - "wasmparser", -] - -[[package]] -name = "wasm-metadata" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" -dependencies = [ - "anyhow", - "indexmap 2.14.0", - "wasm-encoder", - "wasmparser", -] - -[[package]] -name = "wasmparser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" -dependencies = [ - "bitflags", - "hashbrown 0.15.5", - "indexmap 2.14.0", - "semver 1.0.28", -] - -[[package]] -name = "web-time" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" -dependencies = [ - "windows-core 0.52.0", - "windows-targets", -] - -[[package]] -name = "windows-core" -version = "0.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" -dependencies = [ - "windows-targets", -] - -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement", - "windows-interface", - "windows-link", - "windows-result", - "windows-strings", -] - -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "windows-targets" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", - "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", -] - -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_i686_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "winnow" -version = "0.7.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" -dependencies = [ - "memchr", -] - -[[package]] -name = "winnow" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" -dependencies = [ - "wit-bindgen-rust-macro", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "wit-bindgen-core" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" -dependencies = [ - "anyhow", - "heck", - "wit-parser", -] - -[[package]] -name = "wit-bindgen-rust" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" -dependencies = [ - "anyhow", - "heck", - "indexmap 2.14.0", - "prettyplease", - "syn 2.0.117", - "wasm-metadata", - "wit-bindgen-core", - "wit-component", -] - -[[package]] -name = "wit-bindgen-rust-macro" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" -dependencies = [ - "anyhow", - "prettyplease", - "proc-macro2", - "quote", - "syn 2.0.117", - "wit-bindgen-core", - "wit-bindgen-rust", -] - -[[package]] -name = "wit-component" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" -dependencies = [ - "anyhow", - "bitflags", - "indexmap 2.14.0", - "log", - "serde", - "serde_derive", - "serde_json", - "wasm-encoder", - "wasm-metadata", - "wasmparser", - "wit-parser", -] - -[[package]] -name = "wit-parser" -version = "0.244.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" -dependencies = [ - "anyhow", - "id-arena", - "indexmap 2.14.0", - "log", - "semver 1.0.28", - "serde", - "serde_derive", - "serde_json", - "unicode-xid", - "wasmparser", -] - -[[package]] -name = "writeable" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "yoke" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" -dependencies = [ - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.48" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerofrom" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85a5b4158499876c763cb03bc4e49185d3cccbabb15b33c627f7884f43db852e" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zerotrie" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", -] - -[[package]] -name = "zerovec" -version = "0.11.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.11.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.117", -] - -[[package]] -name = "zkhash" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4352d1081da6922701401cdd4cbf29a2723feb4cfabb5771f6fee8e9276da1c7" -dependencies = [ - "ark-ff 0.4.2", - "ark-std 0.4.0", - "bitvec", - "blake2", - "bls12_381 0.7.1", - "byteorder", - "cfg-if", - "group 0.12.1", - "group 0.13.0", - "halo2", - "hex", - "jubjub", - "lazy_static", - "pasta_curves 0.5.1", - "rand 0.8.6", - "serde", - "sha2 0.10.9", - "sha3", - "subtle", -] - -[[package]] -name = "zkm-core-executor" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "anyhow", - "bincode", - "bytemuck", - "elf", - "enum-map", - "env_logger", - "eyre", - "hashbrown 0.14.5", - "hex", - "itertools 0.13.0", - "log", - "nohash-hasher", - "num", - "num_enum", - "p3-field", - "p3-koala-bear", - "p3-maybe-rayon", - "p3-symmetric", - "rand 0.8.6", - "rayon-scan", - "serde", - "serde_json", - "sha2 0.10.9", - "strum 0.26.3", - "strum_macros 0.26.4", - "thiserror 1.0.69", - "tiny-keccak", - "tracing", - "tracing-subscriber", - "typenum", - "vec_map", - "zkm-curves", - "zkm-primitives", - "zkm-stark", -] - -[[package]] -name = "zkm-core-machine" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "bincode", - "cfg-if", - "elliptic-curve", - "generic-array 1.4.1", - "hashbrown 0.14.5", - "hex", - "itertools 0.13.0", - "k256", - "log", - "num", - "num_cpus", - "p256", - "p3-air", - "p3-challenger", - "p3-field", - "p3-keccak-air", - "p3-koala-bear", - "p3-matrix", - "p3-maybe-rayon", - "p3-poseidon2", - "p3-uni-stark", - "p3-util", - "rand 0.8.6", - "rayon", - "rayon-scan", - "serde", - "serde_json", - "size", - "snowbridge-amcl", - "static_assertions", - "strum 0.26.3", - "strum_macros 0.26.4", - "tempfile", - "thiserror 1.0.69", - "tiny-keccak", - "tracing", - "tracing-forest", - "tracing-subscriber", - "typenum", - "web-time", - "zkm-core-executor", - "zkm-curves", - "zkm-derive", - "zkm-primitives", - "zkm-stark", -] - -[[package]] -name = "zkm-curves" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "cfg-if", - "curve25519-dalek", - "dashu", - "elliptic-curve", - "generic-array 1.4.1", - "itertools 0.13.0", - "k256", - "num", - "p256", - "p3-field", - "serde", - "snowbridge-amcl", - "thiserror 1.0.69", - "tracing", - "typenum", - "zkm-primitives", - "zkm-stark", -] - -[[package]] -name = "zkm-derive" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "proc-macro2", - "quote", - "syn 1.0.109", -] - -[[package]] -name = "zkm-lib" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "bincode", - "cfg-if", - "elliptic-curve", - "serde", - "sha2 0.10.9", - "zkm-primitives", -] - -[[package]] -name = "zkm-primitives" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "bincode", - "hex", - "lazy_static", - "num-bigint 0.4.6", - "p3-field", - "p3-koala-bear", - "p3-monty-31", - "p3-poseidon2", - "p3-symmetric", - "serde", - "sha2 0.10.9", -] - -[[package]] -name = "zkm-recursion-core" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "backtrace", - "ff 0.13.1", - "hashbrown 0.14.5", - "itertools 0.13.0", - "p3-air", - "p3-bn254-fr", - "p3-challenger", - "p3-commit", - "p3-dft", - "p3-field", - "p3-fri", - "p3-koala-bear", - "p3-matrix", - "p3-maybe-rayon", - "p3-merkle-tree", - "p3-monty-31", - "p3-poseidon2", - "p3-symmetric", - "p3-util", - "rand 0.8.6", - "serde", - "static_assertions", - "thiserror 1.0.69", - "tracing", - "vec_map", - "zkhash", - "zkm-core-machine", - "zkm-derive", - "zkm-primitives", - "zkm-stark", -] - -[[package]] -name = "zkm-stark" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "arrayref", - "hashbrown 0.14.5", - "itertools 0.13.0", - "num-bigint 0.4.6", - "num-traits", - "p3-air", - "p3-challenger", - "p3-circle", - "p3-commit", - "p3-dft", - "p3-field", - "p3-fri", - "p3-keccak", - "p3-koala-bear", - "p3-matrix", - "p3-maybe-rayon", - "p3-mds", - "p3-merkle-tree", - "p3-mersenne-31", - "p3-poseidon2", - "p3-symmetric", - "p3-uni-stark", - "p3-util", - "rand 0.8.6", - "rayon-scan", - "serde", - "strum 0.26.3", - "strum_macros 0.26.4", - "sysinfo", - "tracing", - "tracing-forest", - "tracing-subscriber", - "zkm-derive", - "zkm-primitives", - "zkm-zkvm", -] - -[[package]] -name = "zkm-verifier" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "anyhow", - "bincode", - "hex", - "itertools 0.13.0", - "lazy_static", - "once_cell", - "p3-bn254-fr", - "p3-commit", - "p3-field", - "p3-koala-bear", - "p3-symmetric", - "p3-util", - "rayon", - "serde", - "sha2 0.10.9", - "strum_macros 0.26.4", - "substrate-bn", - "thiserror 2.0.18", - "zkm-core-executor", - "zkm-core-machine", - "zkm-primitives", - "zkm-recursion-core", - "zkm-stark", -] - -[[package]] -name = "zkm-zkvm" -version = "1.2.5" -source = "git+https://github.com/ProjectZKM/Ziren#7c2071eaa8d43d9d0cb76077ec94b9d98f33073e" -dependencies = [ - "bincode", - "cfg-if", - "getrandom 0.2.17", - "lazy_static", - "libm", - "p3-field", - "p3-koala-bear", - "rand 0.8.6", - "serde", - "sha2 0.10.9", - "zkm-lib", - "zkm-primitives", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" - -[[package]] -name = "zstd" -version = "0.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a" -dependencies = [ - "zstd-safe", -] - -[[package]] -name = "zstd-safe" -version = "7.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d" -dependencies = [ - "zstd-sys", -] - -[[package]] -name = "zstd-sys" -version = "2.0.16+zstd.1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748" -dependencies = [ - "cc", - "pkg-config", -] - -[[patch.unused]] -name = "alloy-primitives" -version = "1.0.0" -source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.0.0#1367f9d00583135013224d764b51d63ffcdd9145" - -[[patch.unused]] -name = "alloy-primitives" -version = "1.1.0" -source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.1.0#66386c419c93166f8471d3ace213d41ffcb13635" - -[[patch.unused]] -name = "alloy-primitives" -version = "1.1.2" -source = "git+https://github.com/ziren-patches/core.git?branch=patch-alloy-primitives-1.1.2#288a6856fc6e4160e85903f2af1c4af92ed4ccf9" - -[[patch.unused]] -name = "secp256k1" -version = "0.29.1" -source = "git+https://github.com/ziren-patches/rust-secp256k1?branch=patch-0.29.1#436fc77c1c72fd151f11004bec618d993b9a2f00" diff --git a/circuits/operator-wrapper-proof/guest/Cargo.toml b/circuits/operator-wrapper-proof/guest/Cargo.toml deleted file mode 100644 index e24a035a..00000000 --- a/circuits/operator-wrapper-proof/guest/Cargo.toml +++ /dev/null @@ -1,33 +0,0 @@ -[workspace] - -[package] -name = "guest" -version = "1.1.0" -edition = "2021" -publish = false - -[dependencies] -bitcoin-light-client-circuit = { path = "../../../crates/bitcoin-light-client-circuit" } -verifier = { path = "../../../crates/verifier" } - -# Ziren -zkm-primitives = { git = "https://github.com/ProjectZKM/Ziren" } -zkm-zkvm = { git = "https://github.com/ProjectZKM/Ziren", features = ["verify"] } - -# Statically turns off logging -tracing = { version = "0.1", features = ["max_level_trace"] } - -bitcoin = { version = "0.32.6", features = ["serde", "rand", "secp-recovery"] } -hex = "0.4.3" - -[patch.crates-io] -# Precompile patches -sha2 = { git = "https://github.com/ziren-patches/RustCrypto-hashes", branch = "patch-sha2-0.10.9", package = "sha2" } -bn = { git = "https://github.com/ziren-patches/bn", branch = "patch-0.6.0", package = "substrate-bn" } -k256 = { git = "https://github.com/ziren-patches/elliptic-curves", branch = "patch-k256-0.13.4" } -p256 = { git = "https://github.com/ziren-patches/elliptic-curves", branch = "patch-p256-0.13.2" } -alloy-primitives-v1-0-0 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.0.0" } -alloy-primitives-v1-1-0 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.1.0" } -alloy-primitives-v1-1-2 = { git = "https://github.com/ziren-patches/core.git", package = "alloy-primitives", branch = "patch-alloy-primitives-1.1.2" } -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" } diff --git a/circuits/operator-wrapper-proof/guest/src/main.rs b/circuits/operator-wrapper-proof/guest/src/main.rs deleted file mode 100644 index d4c5903a..00000000 --- a/circuits/operator-wrapper-proof/guest/src/main.rs +++ /dev/null @@ -1,39 +0,0 @@ -#![no_main] -zkm_zkvm::entrypoint!(main); - -use bitcoin_light_client_circuit::{ - decode_operator_public_outputs, hash_operator_constant, zkm_vk_hash_to_raw, -}; -use verifier::verify_groth16_proof; - -pub fn main() { - let operator_proof: Vec = zkm_zkvm::io::read(); - let operator_public_values: Vec = zkm_zkvm::io::read(); - let operator_vk_hash: Vec = zkm_zkvm::io::read(); - let operator_zkm_version: String = zkm_zkvm::io::read(); - let graph_id: [u8; 16] = zkm_zkvm::io::read(); - let genesis_sequencer_commit_txid: [u8; 32] = zkm_zkvm::io::read(); - - verify_groth16_proof( - &operator_proof, - &operator_public_values, - &operator_vk_hash, - &operator_zkm_version, - ) - .expect("Failed to verify operator proof"); - - let expected_constant = hash_operator_constant(graph_id, genesis_sequencer_commit_txid); - let operator_vk_hash_raw = - zkm_vk_hash_to_raw(&operator_vk_hash).expect("Invalid operator vk hash"); - let operator_outputs = decode_operator_public_outputs( - &operator_public_values, - operator_vk_hash_raw, - ) - .expect("Invalid operator public values"); - assert_eq!(operator_outputs.constant, expected_constant); - assert_eq!(operator_outputs.operator_vk_hash, operator_vk_hash_raw); - - zkm_zkvm::io::commit(&operator_vk_hash_raw); - zkm_zkvm::io::commit(&graph_id); - zkm_zkvm::io::commit(&genesis_sequencer_commit_txid); -} diff --git a/circuits/operator-wrapper-proof/host/Cargo.toml b/circuits/operator-wrapper-proof/host/Cargo.toml deleted file mode 100644 index c8eb5ded..00000000 --- a/circuits/operator-wrapper-proof/host/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "operator-wrapper-proof" -version.workspace = true -edition.workspace = true - -[dependencies] -anyhow.workspace = true -bincode.workspace = true -bitcoin = { workspace = true, features = ["serde", "rand", "secp-recovery"] } -bitcoin-light-client-circuit = { workspace = true } -clap = { workspace = true, features = ["derive", "env"] } -dotenv.workspace = true -hex.workspace = true -proof-builder.workspace = true -serde.workspace = true -sha2.workspace = true -tokio = { workspace = true, features = ["full"] } -tracing = { workspace = true, features = ["max_level_trace"] } -tracing-subscriber.workspace = true -zkm-primitives.workspace = true -zkm-prover.workspace = true -zkm-sdk.workspace = true - -[build-dependencies] -zkm-build.workspace = true diff --git a/circuits/operator-wrapper-proof/host/build.rs b/circuits/operator-wrapper-proof/host/build.rs deleted file mode 100644 index dcfa0c53..00000000 --- a/circuits/operator-wrapper-proof/host/build.rs +++ /dev/null @@ -1,5 +0,0 @@ -use zkm_build::build_program; - -fn main() { - build_program("../guest"); -} diff --git a/circuits/operator-wrapper-proof/host/src/lib.rs b/circuits/operator-wrapper-proof/host/src/lib.rs deleted file mode 100644 index 12e181a5..00000000 --- a/circuits/operator-wrapper-proof/host/src/lib.rs +++ /dev/null @@ -1,302 +0,0 @@ -//! Generate operator wrapper proof. -use anyhow::{Context, ensure}; -use bitcoin::{Txid, hashes::Hash}; -use bitcoin_light_client_circuit::{ - OperatorPublicOutputs, decode_operator_public_outputs, hash_operator_constant, - zkm_vk_hash_to_raw, -}; -use clap::Parser; -use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; -use sha2::{Digest, Sha256}; -use std::fs; -use std::str::FromStr; -use std::sync::OnceLock; -use zkm_sdk::{ - HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, - include_elf, -}; - -static ELF_ID: OnceLock = OnceLock::new(); -const OPERATOR_WRAPPER: &[u8] = include_elf!("guest"); - -#[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] -pub struct Args { - #[serde(default)] - #[arg(long, default_value_t = true)] - pub enable: bool, - - #[serde(default)] - #[clap(long, env, default_value = "")] - pub operator_input_proof: String, - - #[serde(default)] - #[clap(long, env, default_value = "")] - pub graph_id: String, - - #[serde(default)] - #[clap(long, env, default_value = "")] - pub genesis_sequencer_commit_txid: String, - - #[serde(default)] - #[clap(long, env, default_value = "wrapper-proof.bin")] - pub output: String, - - #[serde(default = "default_scan_interval")] - #[arg(long, default_value_t = 30)] - pub scan_interval: u64, -} - -fn default_scan_interval() -> u64 { - 30 -} - -impl Default for Args { - fn default() -> Self { - Self { - enable: false, - operator_input_proof: String::new(), - graph_id: String::new(), - genesis_sequencer_commit_txid: String::new(), - output: "wrapper-proof.bin".to_string(), - scan_interval: 30, - } - } -} - -impl LongRunning for Args { - fn rotate(&self) -> Self { - self.clone() - } -} - -#[derive(Clone, Debug)] -pub struct OperatorProofInputs { - pub proof_bytes: Vec, - pub public_values: Vec, - pub vk_hash: Vec, - pub vk_hash_raw: [u8; 32], - pub zkm_version: String, - pub outputs: OperatorPublicOutputs, -} - -pub struct OperatorWrapperProofBuilder { - client: ProverClient, - proving_key: zkm_sdk::ZKMProvingKey, - verifying_key: zkm_sdk::ZKMVerifyingKey, -} - -impl OperatorWrapperProofBuilder { - pub fn new() -> Self { - let client = ProverClient::new(); - let (proving_key, verifying_key) = client.setup(OPERATOR_WRAPPER); - Self { client, proving_key, verifying_key } - } -} - -pub fn parse_hex_array(value: &str) -> anyhow::Result<[u8; N]> { - let bytes = hex::decode(value.trim_start_matches("0x")) - .with_context(|| format!("failed to decode {N}-byte hex value"))?; - ensure!(bytes.len() == N, "expected {N} bytes, got {}", bytes.len()); - Ok(bytes.try_into().expect("length already checked")) -} - -pub fn read_operator_proof_inputs(path: &str) -> anyhow::Result { - let proof_file = - fs::read(path).with_context(|| format!("failed to read operator proof '{path}'"))?; - let public_values_path = format!("{path}.public_inputs.bin"); - let public_values = fs::read(&public_values_path) - .with_context(|| format!("failed to read operator public inputs '{public_values_path}'"))?; - let vk_hash_path = format!("{path}.vk_hash.bin"); - let vk_hash = fs::read(&vk_hash_path) - .with_context(|| format!("failed to read operator vk hash '{vk_hash_path}'"))?; - let zkm_version_path = format!("{path}.zkm_version.bin"); - let zkm_version = fs::read(&zkm_version_path) - .with_context(|| format!("failed to read operator zkm version '{zkm_version_path}'")) - .and_then(|raw| { - String::from_utf8(raw).with_context(|| format!("invalid UTF-8 in '{zkm_version_path}'")) - })?; - - let proof_bytes = - if let Ok(proof) = bincode::deserialize::(&proof_file) { - ensure!( - proof.public_values.to_vec() == public_values, - "operator proof public values do not match sidecar" - ); - ensure!( - proof.zkm_version == zkm_version, - "operator proof zkm_version does not match sidecar" - ); - proof.bytes() - } else { - proof_file - }; - - let raw_vk_hash = zkm_vk_hash_to_raw(&vk_hash).map_err(anyhow::Error::msg)?; - let outputs = - decode_operator_public_outputs(&public_values, raw_vk_hash).map_err(anyhow::Error::msg)?; - let vk_hash_raw = validate_operator_vk_hash_binding(&outputs, &vk_hash)?; - - Ok(OperatorProofInputs { - proof_bytes, - public_values, - vk_hash, - vk_hash_raw, - zkm_version, - outputs, - }) -} - -pub fn validate_operator_vk_hash_binding( - outputs: &OperatorPublicOutputs, - vk_hash: &[u8], -) -> anyhow::Result<[u8; 32]> { - let raw_vk_hash = zkm_vk_hash_to_raw(vk_hash).map_err(anyhow::Error::msg)?; - ensure!( - outputs.operator_vk_hash == raw_vk_hash, - "operator vk hash does not match authenticated operator public output" - ); - Ok(raw_vk_hash) -} - -impl ProofBuilder for OperatorWrapperProofBuilder { - fn client(&self) -> &zkm_sdk::ProverClient { - &self.client - } - - fn pk(&self) -> &zkm_sdk::ZKMProvingKey { - &self.proving_key - } - - fn vk(&self) -> &zkm_sdk::ZKMVerifyingKey { - &self.verifying_key - } - - fn name() -> String { - "operator-wrapper".to_string() - } - - #[tracing::instrument(level = "info", skip(self, ctx))] - fn build_proof( - &self, - ctx: &ProofRequest, - ) -> anyhow::Result<(Vec, ZKMProofWithPublicValues, u64, f32)> { - let ProofRequest::WrapperProofRequest { - operator_input_proof, - graph_id, - genesis_sequencer_commit_txid, - .. - } = ctx - else { - anyhow::bail!("Invalid proof request type"); - }; - - let operator_inputs = read_operator_proof_inputs(operator_input_proof)?; - let genesis_txid = Txid::from_str(genesis_sequencer_commit_txid) - .with_context(|| format!("invalid genesis txid '{genesis_sequencer_commit_txid}'"))?; - let genesis_txid_bytes = genesis_txid.to_byte_array(); - let expected_constant = hash_operator_constant(*graph_id, genesis_txid_bytes); - ensure!( - operator_inputs.outputs.constant == expected_constant, - "operator public constant does not match wrapper session" - ); - let (proof, cycles, proving_time) = tracing::info_span!("generate wrapper proof") - .in_scope(|| -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { - let mut stdin = ZKMStdin::new(); - stdin.write(&operator_inputs.proof_bytes); - stdin.write(&operator_inputs.public_values); - stdin.write(&operator_inputs.vk_hash); - stdin.write(&operator_inputs.zkm_version); - stdin.write(graph_id); - stdin.write(&genesis_txid_bytes); - - let elf_id = if ELF_ID.get().is_none() { - ELF_ID - .set(hex::encode(Sha256::digest(&self.proving_key.elf))) - .map_err(anyhow::Error::msg)?; - None - } else { - Some(ELF_ID.get().unwrap().clone()) - }; - tracing::info!("elf id: {:?}", elf_id); - - let proving_start = tokio::time::Instant::now(); - let (proof, cycles) = self.client.prove_with_cycles( - &self.proving_key, - &stdin, - ZKMProofKind::Groth16, - elf_id, - )?; - let proving_duration = proving_start.elapsed().as_secs_f32() * 1000.0; - Ok((proof, cycles, proving_duration)) - })?; - - Ok((vec![], proof, cycles, proving_time)) - } - - fn save_proof( - &self, - ctx: &ProofRequest, - _input: &[u8], - _cycles: u64, - proof: ZKMProofWithPublicValues, - ) -> anyhow::Result<(String, usize)> { - let ProofRequest::WrapperProofRequest { output, .. } = ctx else { - anyhow::bail!("invalid context"); - }; - let public_value_hex = hex::encode(proof.public_values.to_vec()); - let serialized_proof = bincode::serialize(&proof)?; - let proof_size = serialized_proof.len(); - let zkm_version = proof.zkm_version.clone(); - fs::write(format!("{output}.public_inputs.bin"), proof.public_values.to_vec())?; - fs::write(format!("{output}.vk_hash.bin"), self.verifying_key.bytes32())?; - fs::write(format!("{output}.zkm_version.bin"), zkm_version)?; - fs::write(output, serialized_proof)?; - Ok((public_value_hex, proof_size)) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use bitcoin_light_client_circuit::zkm_vk_hash_from_raw; - - #[derive(serde::Serialize, serde::Deserialize)] - struct LegacyOperatorPublicOutputs { - btc_best_block_hash: [u8; 32], - constant: [u8; 32], - included_watchtowers: [u8; 32], - } - - #[test] - fn decode_operator_public_outputs_accepts_legacy_public_values() { - let actual_vk_hash = [4u8; 32]; - let legacy_outputs = LegacyOperatorPublicOutputs { - btc_best_block_hash: [0u8; 32], - constant: [1u8; 32], - included_watchtowers: [2u8; 32], - }; - let public_values = bincode::serialize(&legacy_outputs).unwrap(); - - let outputs = decode_operator_public_outputs(&public_values, actual_vk_hash).unwrap(); - - assert_eq!(outputs.btc_best_block_hash, legacy_outputs.btc_best_block_hash); - assert_eq!(outputs.constant, legacy_outputs.constant); - assert_eq!(outputs.included_watchtowers, legacy_outputs.included_watchtowers); - assert_eq!(outputs.operator_vk_hash, actual_vk_hash); - } - - #[test] - fn validate_operator_vk_hash_binding_rejects_mismatched_sidecar() { - let outputs = OperatorPublicOutputs { - btc_best_block_hash: [0u8; 32], - constant: [1u8; 32], - included_watchtowers: [2u8; 32], - operator_vk_hash: [3u8; 32], - }; - let mismatched = zkm_vk_hash_from_raw(&[4u8; 32]); - - let err = validate_operator_vk_hash_binding(&outputs, &mismatched).unwrap_err(); - - assert!(err.to_string().contains("operator vk hash does not match")); - } -} diff --git a/circuits/operator-wrapper-proof/host/src/main.rs b/circuits/operator-wrapper-proof/host/src/main.rs deleted file mode 100644 index df1bc2b5..00000000 --- a/circuits/operator-wrapper-proof/host/src/main.rs +++ /dev/null @@ -1,24 +0,0 @@ -//! Generate operator wrapper proof. -use clap::Parser; -use operator_wrapper_proof::{Args, OperatorWrapperProofBuilder, parse_hex_array}; -use proof_builder::{ProofBuilder, ProofRequest}; - -#[tokio::main] -async fn main() { - dotenv::dotenv().ok(); - let args = Args::parse(); - zkm_sdk::utils::setup_logger(); - - let builder = OperatorWrapperProofBuilder::new(); - let ctx = ProofRequest::WrapperProofRequest { - operator_proof_id: 0, - operator_input_proof: args.operator_input_proof.clone(), - graph_id: parse_hex_array::<16>(&args.graph_id).unwrap(), - genesis_sequencer_commit_txid: args.genesis_sequencer_commit_txid.clone(), - output: args.output.clone(), - }; - - let (input, proof, cycles, _) = builder.build_proof(&ctx).unwrap(); - tracing::info!("Operator wrapper proof cycles: {cycles}"); - builder.save_proof(&ctx, &input, cycles, proof).unwrap(); -} diff --git a/circuits/proof-builder/src/lib.rs b/circuits/proof-builder/src/lib.rs index 9a992e18..ad3081a2 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -68,13 +68,6 @@ pub enum ProofRequest { watchtower_challenge_txn_pubkeys: Vec, watchtower_challenge_txn_scripts: Vec, }, - WrapperProofRequest { - operator_proof_id: i64, - operator_input_proof: String, - graph_id: [u8; 16], - genesis_sequencer_commit_txid: String, - output: String, - }, } #[derive(Error, Debug, Clone)] @@ -140,7 +133,6 @@ pub enum ProofType { StateChain, Operator, Watchtower, - Wrapper, } const HEADER_CHAIN_NAME: &str = "header-chain"; @@ -148,7 +140,6 @@ const COMMIT_CHAIN_NAME: &str = "commit-chain"; const STATE_CHAIN_NAME: &str = "state-chain"; const OPERATOR_NAME: &str = "operator"; const WATCHTOWER_NAME: &str = "watchtower"; -const WRAPPER_NAME: &str = "wrapper"; impl ProofType { pub fn get_chain_name(&self) -> &'static str { match self { @@ -157,7 +148,6 @@ impl ProofType { ProofType::StateChain => STATE_CHAIN_NAME, ProofType::Operator => OPERATOR_NAME, ProofType::Watchtower => WATCHTOWER_NAME, - ProofType::Wrapper => WRAPPER_NAME, } } } @@ -192,14 +182,6 @@ pub struct OperatorProofDescRequest { pub graph_id: String, } -#[derive(Debug, Deserialize)] -pub struct WrapperProofDescRequest { - pub operator_proof_id: Option, - pub instance_id: Option, - pub graph_id: Option, - pub genesis_sequencer_commit_txid: Option, -} - #[derive(Debug, Serialize, Deserialize, Default)] pub struct ProofDescResponse { pub proof_desc: Option, @@ -234,8 +216,7 @@ impl ProofData { | ProofType::CommitChain | ProofType::StateChain | ProofType::Watchtower - | ProofType::Operator - | ProofType::Wrapper => { + | ProofType::Operator => { proof_data.proof = fs::read(path).unwrap_or_default(); proof_data.public_inputs = fs::read(format!("{path}.public_inputs.bin")).unwrap_or_default(); @@ -258,35 +239,6 @@ pub struct OperatorProofResponse { pub error: Option, } -#[derive(Debug, Serialize, Deserialize, Default)] -pub struct WrapperProofMetadata { - pub id: i64, - pub operator_proof_id: i64, - pub instance_id: String, - pub graph_id: String, - pub operator_path_to_proof: String, - pub path_to_proof: Option, - pub public_value_hex: Option, - pub operator_vk_hash: String, - pub genesis_sequencer_commit_txid: String, - pub operator_public_value_hex: Option, - pub proof_state: i64, - pub proof_size: i64, - pub cycles: i64, - pub total_time_to_proof: i64, - pub proving_time: i64, - pub zkm_version: String, - pub created_at: i64, - pub updated_at: i64, -} - -#[derive(Debug, Serialize, Deserialize)] -pub struct WrapperProofResponse { - pub proof_data: Option, - pub metadata: Option, - pub error: Option, -} - #[derive(Debug, Serialize, Deserialize)] pub struct WatchtowerProofRequest { pub instance_id: String, @@ -330,38 +282,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_omits_legacy_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(); - - let proof_data = ProofData::load_proof_data(&base_str, ProofType::Watchtower); - let ProofData { proof, vk, public_inputs, zkm_version } = proof_data; - - assert_eq!(proof, vec![1u8, 2, 3]); - assert_eq!(public_inputs, vec![4u8, 5, 6]); - assert_eq!(vk, "vk-hash"); - assert_eq!(zkm_version, "v1.2.5"); - - 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")); - } -} diff --git a/circuits/run-operator-wrapper-proof.sh b/circuits/run-operator-wrapper-proof.sh deleted file mode 100755 index f5482db1..00000000 --- a/circuits/run-operator-wrapper-proof.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash -set -euo pipefail - -source ~/.zkm-toolchain/env -set -a -source .env -set +a - -require_env() { - local name="$1" - if [ -z "${!name:-}" ]; then - echo "Missing required environment variable: $name" >&2 - exit 1 - fi -} - -require_file() { - local path="$1" - if [ ! -f "$path" ]; then - echo "Missing required file: $path" >&2 - exit 1 - fi -} - -operator_input_proof="${1:-}" -output="${2:-}" -if [ -z "$operator_input_proof" ] || [ -z "$output" ]; then - echo "Usage: bash run-operator-wrapper-proof.sh " >&2 - exit 1 -fi - -require_env "GRAPH_ID" -require_env "GENESIS_SEQUENCER_COMMIT_TXID" - -require_file "$operator_input_proof" -require_file "${operator_input_proof}.public_inputs.bin" -require_file "${operator_input_proof}.vk_hash.bin" -require_file "${operator_input_proof}.zkm_version.bin" - -mkdir -p "$(dirname "$output")" - -graph_id_hex="${GRAPH_ID//-/}" - -cargo build -r --bin operator-wrapper-proof --package operator-wrapper-proof -CMD="../target/release/operator-wrapper-proof" - -RUST_LOG=info "$CMD" \ - --operator-input-proof "$operator_input_proof" \ - --graph-id "$graph_id_hex" \ - --genesis-sequencer-commit-txid "$GENESIS_SEQUENCER_COMMIT_TXID" \ - --output "$output" diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index 922500e4..df14b446 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -47,32 +47,19 @@ pub struct OperatorPublicOutputs { pub btc_best_block_hash: [u8; 32], pub constant: [u8; 32], pub included_watchtowers: [u8; 32], - pub operator_vk_hash: [u8; 32], -} - -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] -struct LegacyOperatorPublicOutputs { - btc_best_block_hash: [u8; 32], - constant: [u8; 32], - included_watchtowers: [u8; 32], } pub fn decode_operator_public_outputs( public_values: &[u8], - actual_operator_vk_hash: [u8; 32], ) -> Result { - if let Ok(outputs) = bincode::deserialize::(public_values) { - return Ok(outputs); + if public_values.len() != 96 { + return Err(format!( + "operator public values must be 96 bytes, got {}", + public_values.len() + )); } - - let legacy_outputs = bincode::deserialize::(public_values) - .map_err(|err| format!("failed to decode operator public values: {err}"))?; - Ok(OperatorPublicOutputs { - btc_best_block_hash: legacy_outputs.btc_best_block_hash, - constant: legacy_outputs.constant, - included_watchtowers: legacy_outputs.included_watchtowers, - operator_vk_hash: actual_operator_vk_hash, - }) + bincode::deserialize(public_values) + .map_err(|err| format!("failed to decode operator public values: {err}")) } pub fn watch_longest_chain( @@ -226,8 +213,7 @@ pub fn propose_longest_chain( state_chain: StateChainCircuitInput, spv_ss_commit: SPV, operator_committed_blockhash: [u8; 32], - actual_operator_vk_hash: [u8; 32], -) -> ([u8; 32], [u8; 32], [u8; 32], [u8; 32]) { +) -> ([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 verify_groth16_proof( @@ -466,26 +452,7 @@ pub fn propose_longest_chain( ); //operator_public_input - ( - operator_committed_blockhash, - constant, - included_watchtowers.to_le_bytes::<32>(), - resolve_operator_vk_hash( - commit_chain_output.chain_state.operator_vk_hash, - actual_operator_vk_hash, - ), - ) -} - -pub fn resolve_operator_vk_hash( - commit_chain_operator_vk_hash: [u8; 32], - actual_operator_vk_hash: [u8; 32], -) -> [u8; 32] { - if commit_chain_operator_vk_hash == commit_chain::LEGACY_OPERATOR_VK_HASH { - actual_operator_vk_hash - } else { - commit_chain_operator_vk_hash - } + (operator_committed_blockhash, constant, included_watchtowers.to_le_bytes::<32>()) } pub fn hash_operator_constant( @@ -499,38 +466,6 @@ pub fn hash_operator_constant( *hash.as_byte_array() } -pub const WRAPPER_PUBLIC_VALUES_SIZE: usize = 32 + GRAPH_ID_SIZE + 32; - -pub fn wrapper_public_values( - operator_vk_hash: [u8; 32], - graph_id: [u8; GRAPH_ID_SIZE], - genesis_sequencer_commit_txid: [u8; 32], -) -> [u8; WRAPPER_PUBLIC_VALUES_SIZE] { - let mut public_values = [0u8; WRAPPER_PUBLIC_VALUES_SIZE]; - public_values[..32].copy_from_slice(&operator_vk_hash); - public_values[32..32 + GRAPH_ID_SIZE].copy_from_slice(&graph_id); - public_values[32 + GRAPH_ID_SIZE..].copy_from_slice(&genesis_sequencer_commit_txid); - public_values -} - -// zkm_vk_hash: 66 bytes, prefix with '0x' -pub fn zkm_vk_hash_to_raw(vk_hash: &[u8]) -> Result<[u8; 32], String> { - let vk_hash = - std::str::from_utf8(vk_hash).map_err(|err| format!("invalid zkm vk hash UTF-8: {err}"))?; - let Some(hex_hash) = vk_hash.strip_prefix("0x") else { - return Err("zkm vk hash must have 0x prefix".to_string()); - }; - if hex_hash.len() != 64 { - return Err(format!("zkm vk hash must be 64 hex chars, got {}", hex_hash.len())); - } - let bytes = hex::decode(hex_hash).map_err(|err| format!("invalid zkm vk hash hex: {err}"))?; - bytes.try_into().map_err(|_| "zkm vk hash must decode to 32 bytes".to_string()) -} - -pub fn zkm_vk_hash_from_raw(raw: &[u8; 32]) -> Vec { - format!("0x{}", hex::encode(raw)).into_bytes() -} - pub fn hash_partial_binding_witness( constant: [u8; 32], btc_best_block_hash: [u8; 32], @@ -848,20 +783,4 @@ mod tests { assert!(parse_watchtower_commitment(&commitment).is_err()); } - - #[test] - fn test_zkm_vk_hash_to_raw_accepts_prefixed_hash() { - let raw = [0xabu8; 32]; - let prefixed = zkm_vk_hash_from_raw(&raw); - - assert_eq!(zkm_vk_hash_to_raw(&prefixed).unwrap(), raw); - } - - #[test] - fn test_zkm_vk_hash_to_raw_rejects_unprefixed_hash() { - let raw = [0xcdu8; 32]; - let unprefixed = hex::encode(raw); - - assert!(zkm_vk_hash_to_raw(unprefixed.as_bytes()).is_err()); - } } diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 0f34a868..9dc093f0 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -369,7 +369,7 @@ pub fn extract_gc_circuit_data( }) } -/// Builds the native BABE assertion witness from the validated wrapper Groth16 proof. +/// 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, diff --git a/crates/store/migrations/20260622000000_drop_wrapper_proof_table.sql b/crates/store/migrations/20260622000000_drop_wrapper_proof_table.sql new file mode 100644 index 00000000..4a542631 --- /dev/null +++ b/crates/store/migrations/20260622000000_drop_wrapper_proof_table.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS wrapper_proof; diff --git a/crates/store/src/localdb.rs b/crates/store/src/localdb.rs index c6e0e590..bdd557f9 100644 --- a/crates/store/src/localdb.rs +++ b/crates/store/src/localdb.rs @@ -2,8 +2,8 @@ use crate::utils::{QueryBuilder, QueryParam, create_place_holders}; use crate::{ BridgeOutGlobalStats, GoatTxRecord, Graph, GraphBtcTxVoutMonitor, GraphRawData, Instance, LongRunningTaskProof, Message, Node, NodesOverview, OperatorProof, PeginGraphProcessData, - PeginInstanceProcessData, PendingGraphInit, ProofState, SequencerSetHashChange, - SequencerSetScanState, SerializableTxid, WatchContract, WatchtowerProof, WrapperProof, + PeginInstanceProcessData, PendingGraphInit, SequencerSetHashChange, SequencerSetScanState, + SerializableTxid, WatchContract, WatchtowerProof, }; use indexmap::IndexMap; @@ -2818,223 +2818,6 @@ impl<'a> StorageProcessor<'a> { Ok(res) } - pub async fn create_wrapper_proof( - &mut self, - wrapper_proof: &WrapperProof, - ) -> anyhow::Result { - let res = sqlx::query( - "INSERT INTO wrapper_proof ( - operator_proof_id, - instance_id, - graph_id, - execution_layer_block_number, - operator_path_to_proof, - path_to_proof, - public_value_hex, - operator_vk_hash, - genesis_sequencer_commit_txid, - operator_public_value_hex, - proof_size, - cycles, - proof_state, - total_time_to_proof, - proving_time, - zkm_version, - extra, - created_at, - updated_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - ) - .bind(wrapper_proof.operator_proof_id) - .bind(wrapper_proof.instance_id) - .bind(wrapper_proof.graph_id) - .bind(wrapper_proof.execution_layer_block_number) - .bind(&wrapper_proof.operator_path_to_proof) - .bind(&wrapper_proof.path_to_proof) - .bind(&wrapper_proof.public_value_hex) - .bind(&wrapper_proof.operator_vk_hash) - .bind(&wrapper_proof.genesis_sequencer_commit_txid) - .bind(&wrapper_proof.operator_public_value_hex) - .bind(wrapper_proof.proof_size) - .bind(wrapper_proof.cycles) - .bind(wrapper_proof.proof_state) - .bind(wrapper_proof.total_time_to_proof) - .bind(wrapper_proof.proving_time) - .bind(&wrapper_proof.zkm_version) - .bind(&wrapper_proof.extra) - .bind(wrapper_proof.created_at) - .bind(wrapper_proof.updated_at) - .execute(self.conn()) - .await?; - Ok(res.rows_affected()) - } - - pub async fn find_wrapper_proof_by_operator_proof_id( - &mut self, - operator_proof_id: i64, - ) -> anyhow::Result> { - let res = sqlx::query_as::<_, WrapperProof>( - "SELECT * FROM wrapper_proof - WHERE operator_proof_id = ? - LIMIT 1", - ) - .bind(operator_proof_id) - .fetch_optional(self.conn()) - .await?; - Ok(res) - } - - pub async fn find_wrapper_proof_by_instance_and_graph( - &mut self, - instance_id: &Uuid, - graph_id: &Uuid, - ) -> anyhow::Result> { - let res = sqlx::query_as::<_, WrapperProof>( - "SELECT * FROM wrapper_proof - WHERE instance_id = ? - AND graph_id = ? - ORDER BY id DESC - LIMIT 1", - ) - .bind(instance_id) - .bind(graph_id) - .fetch_optional(self.conn()) - .await?; - Ok(res) - } - - pub async fn find_next_wrapper_proof(&mut self) -> anyhow::Result> { - let res = sqlx::query_as::<_, WrapperProof>( - "SELECT * FROM wrapper_proof - WHERE proof_state = ? - ORDER BY id ASC - LIMIT 1", - ) - .bind(ProofState::New.to_i64()) - .fetch_optional(self.conn()) - .await?; - Ok(res) - } - - pub async fn claim_next_wrapper_proof(&mut self) -> anyhow::Result> { - let Some(candidate) = self.find_next_wrapper_proof().await? else { - return Ok(None); - }; - - let current_time = get_current_timestamp_secs(); - let updated = sqlx::query( - "UPDATE wrapper_proof - SET proof_state = ?, - updated_at = ? - WHERE id = ? - AND proof_state = ?", - ) - .bind(ProofState::Proving.to_i64()) - .bind(current_time) - .bind(candidate.id) - .bind(ProofState::New.to_i64()) - .execute(self.conn()) - .await? - .rows_affected(); - - if updated == 0 { - return Ok(None); - } - - let res = sqlx::query_as::<_, WrapperProof>( - "SELECT * FROM wrapper_proof - WHERE id = ? - LIMIT 1", - ) - .bind(candidate.id) - .fetch_optional(self.conn()) - .await?; - Ok(res) - } - - pub async fn find_proven_operator_proofs_without_wrapper( - &mut self, - limit: i64, - ) -> anyhow::Result> { - let res = sqlx::query_as::<_, OperatorProof>( - "SELECT operator_proof.* - FROM operator_proof - WHERE operator_proof.proof_state = ? - AND operator_proof.path_to_proof IS NOT NULL - AND NOT EXISTS ( - SELECT 1 - FROM wrapper_proof - WHERE wrapper_proof.operator_proof_id = operator_proof.id - ) - ORDER BY operator_proof.id ASC - LIMIT ?", - ) - .bind(ProofState::Proven.to_i64()) - .bind(limit) - .fetch_all(self.conn()) - .await?; - Ok(res) - } - - #[allow(clippy::too_many_arguments)] - pub async fn update_wrapper_proof_success( - &mut self, - id: i64, - path_to_proof: String, - public_value_hex: String, - proof_size: i64, - cycles: i64, - proving_time: i64, - zkm_version: &str, - ) -> anyhow::Result { - let current_time = get_current_timestamp_secs(); - let res = sqlx::query( - "UPDATE wrapper_proof - SET path_to_proof = ?, - public_value_hex = ?, - proof_size = ?, - cycles = ?, - proof_state = ?, - total_time_to_proof = CASE - WHEN created_at > 0 THEN (? - created_at) * 1000 - ELSE total_time_to_proof - END, - proving_time = ?, - zkm_version = ?, - updated_at = ? - WHERE id = ?", - ) - .bind(path_to_proof) - .bind(public_value_hex) - .bind(proof_size) - .bind(cycles) - .bind(ProofState::Proven.to_i64()) - .bind(current_time) - .bind(proving_time) - .bind(zkm_version) - .bind(current_time) - .bind(id) - .execute(self.conn()) - .await?; - Ok(res.rows_affected()) - } - - pub async fn update_wrapper_proof_failure(&mut self, id: i64) -> anyhow::Result { - let current_time = get_current_timestamp_secs(); - let res = sqlx::query( - "UPDATE wrapper_proof - SET proof_state = ?, - updated_at = ? - WHERE id = ?", - ) - .bind(ProofState::Failed.to_i64()) - .bind(current_time) - .bind(id) - .execute(self.conn()) - .await?; - Ok(res.rows_affected()) - } - #[allow(clippy::too_many_arguments)] pub async fn update_watchtower_proof( &mut self, @@ -3453,79 +3236,4 @@ mod sequencer_set_tests { assert_eq!(state.next_cosmos_block_height, 200); assert_eq!(state.latest_validators_hash, "ddeeff"); } - - #[tokio::test] - async fn test_wrapper_proof_lifecycle() { - let db = setup_db().await; - let mut s = db.acquire().await.unwrap(); - let instance_id = Uuid::parse_str("00112233445566778899aabbccddeeff").unwrap(); - let graph_id = Uuid::parse_str("ffeeddccbbaa99887766554433221100").unwrap(); - let now = get_current_timestamp_secs(); - - let task = WrapperProof { - operator_proof_id: 42, - instance_id, - graph_id, - execution_layer_block_number: 9511055, - operator_path_to_proof: "operator.bin".to_string(), - operator_vk_hash: "operator-vk-hash".to_string(), - genesis_sequencer_commit_txid: - "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246".to_string(), - operator_public_value_hex: Some("abcd".to_string()), - proof_state: ProofState::New.to_i64(), - created_at: now, - updated_at: now, - ..Default::default() - }; - - s.create_wrapper_proof(&task).await.unwrap(); - let duplicate = s.create_wrapper_proof(&task).await; - assert!(duplicate.is_err(), "operator_proof_id must be unique"); - - let found = s - .find_wrapper_proof_by_operator_proof_id(42) - .await - .unwrap() - .expect("wrapper proof should be found"); - assert_eq!(found.instance_id, instance_id); - assert_eq!(found.graph_id, graph_id); - assert_eq!(found.proof_state, ProofState::New.to_i64()); - - let claimed = s - .claim_next_wrapper_proof() - .await - .unwrap() - .expect("new wrapper proof should be claimable"); - assert_eq!(claimed.id, found.id); - assert_eq!(claimed.proof_state, ProofState::Proving.to_i64()); - - s.update_wrapper_proof_success( - claimed.id, - "wrapper.bin".to_string(), - "wrapper-public-value-hex".to_string(), - 1234, - 5678, - 90, - "v1.2.5", - ) - .await - .unwrap(); - let proven = s - .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) - .await - .unwrap() - .expect("wrapper proof should be found by instance and graph"); - assert_eq!(proven.proof_state, ProofState::Proven.to_i64()); - assert_eq!(proven.path_to_proof.as_deref(), Some("wrapper.bin")); - assert_eq!(proven.public_value_hex.as_deref(), Some("wrapper-public-value-hex")); - - s.update_wrapper_proof_failure(proven.id).await.unwrap(); - let failed = s - .find_wrapper_proof_by_operator_proof_id(42) - .await - .unwrap() - .expect("wrapper proof should still exist"); - assert_eq!(failed.proof_state, ProofState::Failed.to_i64()); - assert!(s.claim_next_wrapper_proof().await.unwrap().is_none()); - } } diff --git a/crates/store/src/schema.rs b/crates/store/src/schema.rs index 509ebb1f..72c7eb17 100644 --- a/crates/store/src/schema.rs +++ b/crates/store/src/schema.rs @@ -724,30 +724,6 @@ pub struct OperatorProof { pub updated_at: i64, } -#[derive(Clone, FromRow, Debug, Serialize, Deserialize, Default)] -pub struct WrapperProof { - pub id: i64, - pub operator_proof_id: i64, - pub instance_id: Uuid, - pub graph_id: Uuid, - pub execution_layer_block_number: i64, - pub operator_path_to_proof: String, - pub path_to_proof: Option, - pub public_value_hex: Option, - pub operator_vk_hash: String, - pub genesis_sequencer_commit_txid: String, - pub operator_public_value_hex: Option, - pub proof_size: i64, - pub cycles: i64, - pub proof_state: i64, - pub total_time_to_proof: i64, - pub proving_time: i64, - pub zkm_version: String, - pub extra: Option, - pub created_at: i64, - pub updated_at: i64, -} - #[derive(Clone, FromRow, Debug, Serialize, Deserialize, Default)] pub struct LongRunningTaskProof { pub block_start: i64, diff --git a/deployment/regtest/proof-builder-rpc/proof-builder.toml b/deployment/regtest/proof-builder-rpc/proof-builder.toml index 6c050856..114bacce 100644 --- a/deployment/regtest/proof-builder-rpc/proof-builder.toml +++ b/deployment/regtest/proof-builder-rpc/proof-builder.toml @@ -73,9 +73,3 @@ graph_id="0x00112233445566778899aabbccddeeff" execution_layer_block_number = 9511055 index = 1 bitcoin_network = "regtest" - -[wrapper] -enable = false -output = "../circuits/data/wrapper/output.bin" -genesis_sequencer_commit_txid = "5d5a29e1c426abc4b947f90b93c6b043683aafad79cb3b9a9829dfd5e51cad09" -scan_interval = 30 diff --git a/node/src/env.rs b/node/src/env.rs index d79a46af..092937b5 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -80,8 +80,6 @@ pub const ENV_WATCHTOWER_PROOF_WAIT_SECS: &str = "WATCHTOWER_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_PROOF_WAIT_SECS: &str = "OPERATOR_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_VK_HASH: &str = "OPERATOR_VK_HASH"; pub const ENV_OPERATOR_ZKM_VERSION: &str = "OPERATOR_ZKM_VERSION"; -pub const ENV_OPERATOR_WRAPPER_VK_HASH: &str = "OPERATOR_WRAPPER_VK_HASH"; -pub const ENV_OPERATOR_WRAPPER_ZKM_VERSION: &str = "OPERATOR_WRAPPER_ZKM_VERSION"; pub const DEFAULT_WATCHTOWER_PROOF_WAIT_SECS: usize = 60; pub const DEFAULT_OPERATOR_PROOF_WAIT_SECS: usize = 60; pub const ENV_FGC_GATES_PATH: &str = "FGC_GATES_PATH"; @@ -613,16 +611,6 @@ pub fn get_operator_zkm_version() -> anyhow::Result { .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_ZKM_VERSION} needs to be set")) } -pub fn get_operator_wrapper_vk_hash() -> anyhow::Result { - std::env::var(ENV_OPERATOR_WRAPPER_VK_HASH) - .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_VK_HASH} needs to be set")) -} - -pub fn get_operator_wrapper_zkm_version() -> anyhow::Result { - std::env::var(ENV_OPERATOR_WRAPPER_ZKM_VERSION) - .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_ZKM_VERSION} needs to be set")) -} - pub struct BabeGcAssetPaths { pub fgc_gates: PathBuf, pub fgc_out_indices: PathBuf, diff --git a/node/src/handle.rs b/node/src/handle.rs index e67785c8..e16f75c9 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -1271,7 +1271,7 @@ async fn handle_gen_circuits_operator( let operator_state = state.operator.get_or_insert_with(|| OperatorBabeSetupState { frozen_verifier_pubkeys: None, candidates: vec![], - asserted_wrapper_proof: None, + asserted_operator_proof: None, }); let was_frozen = operator_state.frozen_verifier_pubkeys.is_some(); @@ -3720,20 +3720,20 @@ async fn handle_assert_ready_operator( let dynamic_input = operator_proof.public_inputs[1]; let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; - let mut asserted_wrapper_proof = Vec::new(); - operator_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; + let mut asserted_operator_proof = Vec::new(); + operator_proof.proof.serialize_compressed(&mut asserted_operator_proof)?; let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; let operator_state = setup_state .operator .as_mut() .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; - if let Some(existing) = &operator_state.asserted_wrapper_proof - && existing != &asserted_wrapper_proof + if let Some(existing) = &operator_state.asserted_operator_proof + && existing != &asserted_operator_proof { - bail!("operator assertion wrapper proof conflicts with persisted proof"); + bail!("operator assertion proof conflicts with persisted proof"); } - operator_state.asserted_wrapper_proof = Some(asserted_wrapper_proof); + operator_state.asserted_operator_proof = Some(asserted_operator_proof); save_babe_setup_state(ctx.local_db, instance_id, graph_id, &setup_state)?; let assert_tx = operator_sign_assert(&mut graph, &assert_secret_key, &assert_message)?; @@ -4074,11 +4074,11 @@ async fn handle_challenge_assert_sent_operator( .as_ref() .ok_or_else(|| anyhow!("missing BABE prover state for verifier slot {verifier_index}"))?; let proof_bytes = operator_state - .asserted_wrapper_proof + .asserted_operator_proof .as_ref() - .ok_or_else(|| anyhow!("missing asserted wrapper proof for graph {graph_id}"))?; + .ok_or_else(|| anyhow!("missing asserted operator proof for graph {graph_id}"))?; let proof = Groth16Proof::deserialize_compressed(proof_bytes.as_slice()) - .context("deserialize asserted wrapper proof")?; + .context("deserialize asserted operator proof")?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE wrongly challenged")?; let (_, dyn_pubin) = TxAssertWitness { wots_sig: challenge_witness.witness.wots_sig.clone() } .recover_pi1_xd_without_verify() @@ -4763,7 +4763,7 @@ mod tests { gc_data: None, prover_state: None, }], - asserted_wrapper_proof: None, + asserted_operator_proof: None, } } @@ -4780,7 +4780,7 @@ mod tests { gc_data: None, prover_state: None, }], - asserted_wrapper_proof: None, + asserted_operator_proof: None, }; freeze_operator_candidates(&mut state).unwrap(); diff --git a/node/src/rpc_service/routes.rs b/node/src/rpc_service/routes.rs index 901b4bda..1b023c26 100644 --- a/node/src/rpc_service/routes.rs +++ b/node/src/rpc_service/routes.rs @@ -27,7 +27,6 @@ pub(crate) mod v1 { pub const NODES_WATCHTOWER_BASE: &str = "/v1/proofs/watchtower_proofs"; #[allow(dead_code)] pub const NODES_OPERATOR_BASE: &str = "/v1/proofs/operator_proofs"; - pub const PROOFS_WRAPPER_PROOF: &str = "/v1/proofs/wrapper_proofs"; pub const PROOFS_OPERATOR_PROOF_DESC: &str = "/v1/proofs/operator_proofs_desc"; pub const PROOFS_WATCHTOWER_PROOF_TIMEOUT: &str = "/v1/proofs/watchtower_proofs_timeout"; #[allow(dead_code)] diff --git a/node/src/utils.rs b/node/src/utils.rs index 08c6c0f0..5a52ad2e 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -26,7 +26,6 @@ use bitcoin::{ }; use bitcoin_light_client_circuit::{ VK_HASH_SIZE, build_watchtower_commitment, decode_operator_public_outputs, - wrapper_public_values, }; use bitvm::treepp::*; use bitvm_lib::actors::Actor; @@ -76,7 +75,6 @@ use store::localdb::{ use crate::env; use crate::rpc_service::routes::v1::{ NODES_OPERATOR_BASE, NODES_WATCHTOWER_BASE, PROOFS_WATCHTOWER_PROOF_TIMEOUT, - PROOFS_WRAPPER_PROOF, }; use crate::scheduled_tasks::get_goat_message_content_type; use crate::scheduled_tasks::graph_maintenance_tasks::{ @@ -93,7 +91,7 @@ use client::http_client::async_client::HttpAsyncClient; use proof_builder::{ OperatorProofRequest, OperatorProofResponse, ProofData, WatchtowerProofRequest, WatchtowerProofResponse, WatchtowerProofTimeoutUpdateRequest, - WatchtowerProofTimeoutUpdateResponse, WrapperProofResponse, + WatchtowerProofTimeoutUpdateResponse, }; use store::{ BridgeOutGlobalStats, ByteArray32, Graph, GraphRawData, GraphStatus, Instance, @@ -108,7 +106,7 @@ use zkm_sdk::ZKMProofWithPublicValues; use zkm_stark::PartStarkVerifyingKey; use zkm_verifier::{ Groth16Verifier, IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk, decode_zkm_vkey_hash, - hash_public_inputs, load_ark_public_inputs_from_bytes, + load_ark_public_inputs_from_bytes, }; pub(crate) const BRIDGE_OUT_GLOBAL_STATS_ID: i64 = 1; @@ -127,16 +125,6 @@ pub struct ValidatedOperatorProof { pub zkm_version: String, } -#[derive(Clone)] -pub struct ValidatedOperatorWrapperProof { - pub proof: Groth16Proof, - pub public_inputs: PublicInputs, - pub verifying_key: VerifyingKey, - pub public_values: Vec, - pub wrapper_vk_hash: String, - pub zkm_version: String, -} - #[derive(Clone)] pub struct OperatorStatement { pub static_input: ark_bn254::Fr, @@ -145,14 +133,6 @@ pub struct OperatorStatement { pub constant: [u8; 32], } -#[derive(Clone)] -pub struct OperatorWrapperStatement { - pub public_values: Vec, - pub public_inputs: PublicInputs, - pub wrapper_vk_hash: String, - pub zkm_version: String, -} - pub mod todo_funcs { #![allow(dead_code, unreachable_code, unused_variables)] @@ -2053,30 +2033,6 @@ fn combined_operator_vk_hash(operator_vk_hash: &str, zkm_version: &str) -> Resul Ok(encoded) } -fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { - if !wrapper_vk_hash.starts_with("0x") { - bail!("configured wrapper vk hash must use 0x-prefixed Ziren encoding"); - } - let raw_vk_hash = decode_zkm_vkey_hash(wrapper_vk_hash) - .map_err(|e| anyhow!("invalid configured wrapper vk hash: {e:?}"))?; - let part_vk: PartStarkVerifyingKey = - bincode::deserialize(&load_part_stark_vk_for_zkm_version(zkm_version)?) - .context("deserialize configured wrapper partial STARK verifying key")?; - let base = Bn254Fr::from_canonical_u32(256); - let mut field_hash = Bn254Fr::ZERO; - for byte in raw_vk_hash { - field_hash = field_hash * base + Bn254Fr::from_canonical_u32(byte as u32); - } - let combined = zkm_recursion_core::hash_vkey_with_part_vk(&part_vk, field_hash); - let bytes = combined.as_canonical_biguint().to_bytes_be(); - if bytes.len() > 32 { - bail!("combined wrapper verifying key hash exceeds BN254 field encoding"); - } - let mut encoded = [0u8; 32]; - encoded[32 - bytes.len()..].copy_from_slice(&bytes); - Ok(encoded) -} - pub fn derive_operator_statement(graph_id: Uuid) -> Result { let vk_hash = get_operator_vk_hash()?; let zkm_version = get_operator_zkm_version()?; @@ -2086,161 +2042,6 @@ pub fn derive_operator_statement(graph_id: Uuid) -> Result { Ok(OperatorStatement { static_input, vk_hash, zkm_version, constant }) } -pub fn derive_operator_wrapper_statement(graph_id: Uuid) -> Result { - let public_values = wrapper_public_values( - get_operator_vk_hash()?, - *graph_id.as_bytes(), - get_genesis_sequencer_commit_id(), - ) - .to_vec(); - let wrapper_vk_hash = get_operator_wrapper_vk_hash()?; - let zkm_version = get_operator_wrapper_zkm_version()?; - let public_inputs = load_ark_public_inputs_from_bytes( - &combined_wrapper_vk_hash(&wrapper_vk_hash, &zkm_version)?, - &hash_public_inputs(&public_values), - ) - .to_vec(); - Ok(OperatorWrapperStatement { public_values, public_inputs, wrapper_vk_hash, zkm_version }) -} - -/// Returns: -/// - `Ok(Some(WrapperProof), _)` if wrapper proof is available -/// - `Ok(None, wait_secs)` if operator or wrapper proof is not yet available -pub async fn get_operator_wrapper_proof( - local_db: &LocalDB, - http_client: &HttpAsyncClient, - bitvm_graph: &BitvmGcGraph, - btc_client: &BTCClient, - instance_id: Uuid, - graph_id: Uuid, -) -> Result<(Option, usize)> { - let mut storage_processor = local_db.acquire().await?; - let Some(graph) = storage_processor.find_graph(&graph_id).await? else { - warn!("graph:{graph_id} not found"); - bail!("No graph in db"); - }; - drop(storage_processor); - - if graph.proceed_withdraw_height <= 0 { - warn!("graph {graph_id} proceed_withdraw_height <= 0, waiting to been updated"); - return Ok((None, get_operator_proof_wait_secs())); - } - - let watchtower_challenge_init_txid = graph - .watchtower_challenge_init_txid - .ok_or_else(|| anyhow::anyhow!("watchtower_challenge_init_txid is none"))?; - let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); - let (watchtower_challenge_txids, included_watchtowers) = match get_watchtower_challenge_info( - btc_client, - &watchtower_challenge_init_txid, - num_challenger, - ) - .await - { - Ok(info) => info, - Err(e) => { - warn!("Failed to get watchtower challenge info: {e}"); - return Ok((None, get_operator_proof_wait_secs())); - } - }; - let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( - btc_client, - &watchtower_challenge_txids, - &included_watchtowers, - ) - .await?; - let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); - - let base_url = Url::parse( - &get_proof_build_rpc_host() - .ok_or_else(|| anyhow::anyhow!("failed to get proof_build_rpc_host"))?, - )?; - let operator_url = base_url.join(NODES_OPERATOR_BASE)?; - let operator_response = http_client - .post_response_json::( - operator_url.as_str(), - &OperatorProofRequest { - instance_id: instance_id.to_string(), - graph_id: graph_id.to_string(), - operator_committed_blockhash, - execution_layer_block_number: graph.proceed_withdraw_height, - watchtower_challenge_txids, - included_watchtowers, - watchtower_challenge_init_txid: watchtower_challenge_init_txid.0.to_string(), - watchtower_challenge_pubkeys: bitvm_graph - .parameters - .watchtower_pubkeys - .iter() - .map(|pk| pk.public_key(secp256k1::Parity::Even).to_string()) - .collect(), - }, - ) - .await?; - - if operator_response.proof_data.is_none() { - return Ok((None, get_operator_proof_wait_secs())); - } - - let statement = derive_operator_wrapper_statement(graph_id)?; - let expected_public_values = statement.public_values.clone(); - let genesis_txid_text = std::env::var(ENV_GENESIS_SEQUENCER_COMMIT_TXID) - .map_err(|_| anyhow!("{ENV_GENESIS_SEQUENCER_COMMIT_TXID} needs to be set"))?; - - let mut wrapper_url = base_url.join(PROOFS_WRAPPER_PROOF)?; - wrapper_url - .query_pairs_mut() - .append_pair("instance_id", &instance_id.to_string()) - .append_pair("graph_id", &graph_id.to_string()) - .append_pair("genesis_sequencer_commit_txid", &genesis_txid_text); - let wrapper_response = - http_client.get_response_json::(wrapper_url.as_str()).await?; - - let Some(proof_data) = wrapper_response.proof_data else { - if let Some(error) = wrapper_response.error { - info!("operator wrapper proof is not ready for graph_id:{graph_id}: {error}"); - } - return Ok((None, get_operator_proof_wait_secs())); - }; - - let proof: ZKMProofWithPublicValues = bincode::deserialize(proof_data.proof.as_slice()) - .map_err(|err| anyhow!("failed to deserialize operator wrapper proof: {err}"))?; - let proof_public_values = proof.public_values.to_vec(); - if proof_public_values != expected_public_values { - bail!("operator wrapper proof public values do not match graph challenge inputs"); - } - if !proof_data.public_inputs.is_empty() && proof_data.public_inputs != expected_public_values { - bail!("operator wrapper proof public input sidecar does not match proof"); - } - let expected_wrapper_vk_hash = statement.wrapper_vk_hash.clone(); - if proof_data.vk != expected_wrapper_vk_hash { - bail!("operator wrapper proof vk hash does not match configured wrapper identity"); - } - let expected_zkm_version = statement.zkm_version.clone(); - if proof.zkm_version != expected_zkm_version || proof_data.zkm_version != expected_zkm_version { - bail!("operator wrapper proof Ziren version does not match configured wrapper identity"); - } - - let part_stark_vk = load_part_stark_vk_for_zkm_version(&proof.zkm_version)?; - let ark_proof = - convert_ark_imm_wrap_vk(&proof, &proof_data.vk, &IMM_GROTH16_VK_BYTES, &part_stark_vk) - .map_err(|e| anyhow!("failed to convert operator wrapper proof to ark format: {e}"))?; - if ark_proof.public_inputs.as_slice() != statement.public_inputs.as_slice() { - bail!("operator wrapper proof public inputs do not match graph setup statement"); - } - - Ok(( - Some(ValidatedOperatorWrapperProof { - proof: ark_proof.proof.clone(), - public_inputs: ark_proof.public_inputs.into(), - verifying_key: ark_proof.groth16_vk.into(), - public_values: expected_public_values, - wrapper_vk_hash: expected_wrapper_vk_hash, - zkm_version: expected_zkm_version, - }), - 0, - )) -} - /// Returns: /// - `Ok(Some(OperatorProof), _)` if operator proof is available and valid /// - `Ok(None, wait_secs)` if operator proof is not yet available @@ -2336,7 +2137,7 @@ pub async fn get_operator_proof( bail!("operator proof Ziren version does not match configured operator identity"); } - let outputs = decode_operator_public_outputs(&proof.public_values.to_vec(), statement.vk_hash) + let outputs = decode_operator_public_outputs(&proof.public_values.to_vec()) .map_err(|e| anyhow!("invalid operator public outputs: {e}"))?; if outputs.constant != statement.constant { bail!("operator proof constant does not match graph setup"); @@ -4985,7 +4786,7 @@ pub struct OperatorBabeSetupState { pub frozen_verifier_pubkeys: Option>, pub candidates: Vec, #[serde(default)] - pub asserted_wrapper_proof: Option>, + pub asserted_operator_proof: Option>, } #[cfg(test)] diff --git a/proof-builder-rpc/Cargo.toml b/proof-builder-rpc/Cargo.toml index 68e9b5b3..e478e1de 100644 --- a/proof-builder-rpc/Cargo.toml +++ b/proof-builder-rpc/Cargo.toml @@ -36,7 +36,6 @@ commit-chain = { workspace = true } state-chain-proof = { workspace = true } watchtower-proof = { workspace = true } operator-proof = { workspace = true } -operator-wrapper-proof = { workspace = true } util = { workspace = true } client = { workspace = true } bitcoin-light-client-circuit.workspace = true diff --git a/proof-builder-rpc/proof-builder.toml.example b/proof-builder-rpc/proof-builder.toml.example index c90145c6..96dce504 100644 --- a/proof-builder-rpc/proof-builder.toml.example +++ b/proof-builder-rpc/proof-builder.toml.example @@ -73,9 +73,3 @@ graph_id="86F23E650F6D447885CA320E6BA27ABD" execution_layer_block_number = 10321027 index = 1 bitcoin_network = "regtest" - -[wrapper] -enable = false -output = "../circuits/data/wrapper/output.bin" -genesis_sequencer_commit_txid = "5d5a29e1c426abc4b947f90b93c6b043683aafad79cb3b9a9829dfd5e51cad09" -scan_interval = 30 diff --git a/proof-builder-rpc/src/api/mod.rs b/proof-builder-rpc/src/api/mod.rs index c1f5d999..8ad9cfa0 100644 --- a/proof-builder-rpc/src/api/mod.rs +++ b/proof-builder-rpc/src/api/mod.rs @@ -6,9 +6,9 @@ mod validation; use crate::api::metrics_service::{ApiMetricsState, metrics_handler, metrics_middleware}; use crate::api::proof_handler::{ - get_chain_proof_task_desc, get_operator_proof_task_desc, get_wrapper_proof_task, - get_wrapper_proof_task_desc, post_operator_proof_task, post_watchtower_proof_task, - update_operator_proof_task_timeout, update_watchtower_proof_task_timeout, + get_chain_proof_task_desc, get_operator_proof_task_desc, post_operator_proof_task, + post_watchtower_proof_task, update_operator_proof_task_timeout, + update_watchtower_proof_task_timeout, }; use axum::http::Method; use axum::routing::{get, post}; @@ -48,8 +48,6 @@ pub(crate) async fn serve( .route(routes::v1::PROOFS_OPERATOR_PROOF, post(post_operator_proof_task)) .route(routes::v1::PROOFS_OPERATOR_PROOF_TIMEOUT, post(update_operator_proof_task_timeout)) .route(routes::v1::PROOFS_OPERATOR_PROOF_DESC, get(get_operator_proof_task_desc)) - .route(routes::v1::PROOFS_WRAPPER_PROOF, get(get_wrapper_proof_task)) - .route(routes::v1::PROOFS_WRAPPER_PROOF_DESC, get(get_wrapper_proof_task_desc)) .layer(middleware::from_fn_with_state(api_state.clone(), metrics_middleware)) .layer(CorsLayer::new().allow_headers(Any).allow_origin(Any).allow_methods(vec![ Method::GET, diff --git a/proof-builder-rpc/src/api/proof_handler.rs b/proof-builder-rpc/src/api/proof_handler.rs index dde156e8..e86b1f4e 100644 --- a/proof-builder-rpc/src/api/proof_handler.rs +++ b/proof-builder-rpc/src/api/proof_handler.rs @@ -1,22 +1,20 @@ use crate::api::ApiState; -use crate::api::response::{ApiErrorExt, ApiResult, ErrorResponse, ok_response}; +use crate::api::response::{ApiErrorExt, ApiResult, ok_response}; use crate::api::validation::InputValidator; use crate::task::{ - add_operator_task, add_watchtower_task, create_missing_wrapper_tasks, find_operator_task, - find_watchtower_task, update_operator_task_state, update_watchtower_task_state, + add_operator_task, add_watchtower_task, find_operator_task, find_watchtower_task, + update_operator_task_state, update_watchtower_task_state, }; use axum::Json; use axum::extract::{Query, State}; -use axum::http::StatusCode; use proof_builder::{ ChainProofDescRequest, OperatorProofDescRequest, OperatorProofRequest, OperatorProofResponse, OperatorProofTimeoutUpdateRequest, OperatorProofTimeoutUpdateResponse, ProofData, ProofDesc, ProofDescResponse, ProofType, WatchtowerProofRequest, WatchtowerProofResponse, WatchtowerProofTimeoutUpdateRequest, WatchtowerProofTimeoutUpdateResponse, - WrapperProofDescRequest, WrapperProofMetadata, WrapperProofResponse, }; use std::sync::Arc; -use store::{ProofState, WrapperProof}; +use store::ProofState; use tracing::info; #[axum::debug_handler] @@ -188,62 +186,6 @@ pub(super) async fn post_operator_proof_task( } } -#[axum::debug_handler] -pub(super) async fn get_wrapper_proof_task( - State(api_state): State>, - Query(payload): Query, -) -> ApiResult { - let wrapper_proof = find_wrapper_proof_by_request(&api_state, &payload).await?; - match wrapper_proof { - Some(wrapper_proof) - if wrapper_proof.proof_state == ProofState::Proven.to_i64() - && wrapper_proof.path_to_proof.is_some() => - { - ok_response(WrapperProofResponse { - proof_data: Some(ProofData::load_proof_data( - wrapper_proof.path_to_proof.as_deref().unwrap(), - ProofType::Wrapper, - )), - metadata: Some(wrapper_metadata(&wrapper_proof)), - error: None, - }) - } - Some(wrapper_proof) => ok_response(WrapperProofResponse { - proof_data: None, - metadata: Some(wrapper_metadata(&wrapper_proof)), - error: Some(format!( - "The wrapper proof is not ready, state {}, path:{:?}", - wrapper_proof.proof_state, wrapper_proof.path_to_proof - )), - }), - None => ok_response(WrapperProofResponse { - proof_data: None, - metadata: None, - error: Some("No wrapper proof found".to_string()), - }), - } -} - -#[axum::debug_handler] -pub(super) async fn get_wrapper_proof_task_desc( - State(api_state): State>, - Query(payload): Query, -) -> ApiResult { - let wrapper_proof = find_wrapper_proof_by_request(&api_state, &payload).await?; - match wrapper_proof { - Some(wrapper_proof) => ok_response(WrapperProofResponse { - proof_data: None, - metadata: Some(wrapper_metadata(&wrapper_proof)), - error: None, - }), - None => ok_response(WrapperProofResponse { - proof_data: None, - metadata: None, - error: Some("No wrapper proof found".to_string()), - }), - } -} - #[axum::debug_handler] pub(super) async fn update_operator_proof_task_timeout( State(api_state): State>, @@ -275,78 +217,6 @@ pub(super) async fn update_operator_proof_task_timeout( } } -async fn find_wrapper_proof_by_request( - api_state: &Arc, - payload: &WrapperProofDescRequest, -) -> Result, (StatusCode, Json)> { - if let Some(operator_proof_id) = payload.operator_proof_id { - let mut storage_process = - api_state.local_db.acquire().await.api_error("GET_WRAPPER_PROOF_ERROR")?; - return storage_process - .find_wrapper_proof_by_operator_proof_id(operator_proof_id) - .await - .api_error("GET_WRAPPER_PROOF_ERROR"); - } - - let instance_id = InputValidator::validate_uuid( - payload.instance_id.as_deref().unwrap_or_default(), - "instance_id", - )?; - let graph_id = - InputValidator::validate_uuid(payload.graph_id.as_deref().unwrap_or_default(), "graph_id")?; - - let wrapper_proof = { - let mut storage_process = - api_state.local_db.acquire().await.api_error("GET_WRAPPER_PROOF_ERROR")?; - storage_process - .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) - .await - .api_error("GET_WRAPPER_PROOF_ERROR")? - }; - - if wrapper_proof.is_some() { - return Ok(wrapper_proof); - } - - if let Some(genesis_txid) = payload.genesis_sequencer_commit_txid.as_deref() - && !genesis_txid.is_empty() - { - create_missing_wrapper_tasks(&api_state.local_db, genesis_txid) - .await - .api_error("GET_WRAPPER_PROOF_ERROR")?; - } - - let mut storage_process = - api_state.local_db.acquire().await.api_error("GET_WRAPPER_PROOF_ERROR")?; - storage_process - .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) - .await - .api_error("GET_WRAPPER_PROOF_ERROR") -} - -fn wrapper_metadata(wrapper_proof: &WrapperProof) -> WrapperProofMetadata { - WrapperProofMetadata { - id: wrapper_proof.id, - operator_proof_id: wrapper_proof.operator_proof_id, - instance_id: wrapper_proof.instance_id.to_string(), - graph_id: wrapper_proof.graph_id.to_string(), - operator_path_to_proof: wrapper_proof.operator_path_to_proof.clone(), - path_to_proof: wrapper_proof.path_to_proof.clone(), - public_value_hex: wrapper_proof.public_value_hex.clone(), - operator_vk_hash: wrapper_proof.operator_vk_hash.clone(), - genesis_sequencer_commit_txid: wrapper_proof.genesis_sequencer_commit_txid.clone(), - operator_public_value_hex: wrapper_proof.operator_public_value_hex.clone(), - proof_state: wrapper_proof.proof_state, - proof_size: wrapper_proof.proof_size, - cycles: wrapper_proof.cycles, - total_time_to_proof: wrapper_proof.total_time_to_proof, - proving_time: wrapper_proof.proving_time, - zkm_version: wrapper_proof.zkm_version.clone(), - created_at: wrapper_proof.created_at, - updated_at: wrapper_proof.updated_at, - } -} - #[axum::debug_handler] pub(super) async fn post_watchtower_proof_task( State(api_state): State>, diff --git a/proof-builder-rpc/src/api/routes.rs b/proof-builder-rpc/src/api/routes.rs index df37f6c7..fea597ee 100644 --- a/proof-builder-rpc/src/api/routes.rs +++ b/proof-builder-rpc/src/api/routes.rs @@ -7,6 +7,4 @@ pub(crate) mod v1 { pub(crate) const PROOFS_OPERATOR_PROOF: &str = "/v1/proofs/operator_proofs"; pub(crate) const PROOFS_OPERATOR_PROOF_TIMEOUT: &str = "/v1/proofs/operator_proofs_timeout"; pub(crate) const PROOFS_OPERATOR_PROOF_DESC: &str = "/v1/proofs/operator_proofs_desc"; - pub(crate) const PROOFS_WRAPPER_PROOF: &str = "/v1/proofs/wrapper_proofs"; - pub(crate) const PROOFS_WRAPPER_PROOF_DESC: &str = "/v1/proofs/wrapper_proofs_desc"; } diff --git a/proof-builder-rpc/src/config.rs b/proof-builder-rpc/src/config.rs index c47ff1ce..7c137c5b 100644 --- a/proof-builder-rpc/src/config.rs +++ b/proof-builder-rpc/src/config.rs @@ -9,8 +9,6 @@ pub(crate) struct ProofBuilderConfig { pub state_chain: state_chain_proof::Args, pub watchtower: watchtower_proof::Args, pub operator: operator_proof::Args, - #[serde(default)] - pub wrapper: operator_wrapper_proof::Args, } impl ProofBuilderConfig { diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index 6241a103..949c9d49 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -3,13 +3,11 @@ mod header_chain_proof; mod operator_proof; mod state_chain_proof; mod watchtower_proof; -mod wrapper_proof; use crate::config::ProofBuilderConfig; use crate::task::{ commit_chain_proof::spawn_commit_chain_proof_task, header_chain_proof::spawn_header_chain_proof_task, operator_proof::spawn_operator_proof_task, state_chain_proof::spawn_state_chain_proof_task, watchtower_proof::spawn_watchtower_proof_task, - wrapper_proof::spawn_wrapper_proof_task, }; use ::commit_chain_proof::CommitChainProofBuilder; use ::header_chain_proof::HeaderChainProofBuilder; @@ -20,7 +18,6 @@ use std::collections::HashSet; use std::str::FromStr; use std::time::UNIX_EPOCH; use uuid::Uuid; -pub(crate) use wrapper_proof::create_missing_wrapper_tasks; use futures::future::Either; use proof_builder::{OnDemandTask, ProofBuilder}; @@ -35,7 +32,6 @@ pub(crate) fn is_start_generate_proof_tasks(cfg: &ProofBuilderConfig) -> bool { || cfg.state_chain.enable || cfg.watchtower.enable || cfg.operator.enable - || cfg.wrapper.enable } pub(crate) async fn run_generate_proof_tasks( @@ -104,18 +100,6 @@ pub(crate) async fn run_generate_proof_tasks( Either::Right(std::future::pending::, tokio::task::JoinError>>()) }; - let wrapper_proof_future = if cfg.wrapper.enable { - Either::Left(spawn_wrapper_proof_task( - cfg.wrapper.clone(), - local_db.clone(), - interval, - interval, - cancellation_token.clone(), - )) - } else { - Either::Right(std::future::pending::, tokio::task::JoinError>>()) - }; - tokio::select! { result = header_chain_proof_future => { match result { @@ -192,21 +176,6 @@ pub(crate) async fn run_generate_proof_tasks( } } } - result = wrapper_proof_future => { - match result { - Ok(Ok(_)) => { - info!("Wrapper proof generate task completed successfully"); - } - Ok(Err(e)) => { - error!("Wrapper proof generate task error: {}", e); - return Err(e); - } - Err(e) => { - error!("Wrapper proof generate task panic: {:?}", e); - anyhow::bail!("Wrapper proof generate task panic: {:?}", e); - } - } - } } Ok("tasks_completed".to_string()) diff --git a/proof-builder-rpc/src/task/wrapper_proof.rs b/proof-builder-rpc/src/task/wrapper_proof.rs deleted file mode 100644 index 2078820c..00000000 --- a/proof-builder-rpc/src/task/wrapper_proof.rs +++ /dev/null @@ -1,259 +0,0 @@ -use crate::task::current_time_secs; -use operator_wrapper_proof::OperatorWrapperProofBuilder; -use proof_builder::{ProofBuilder, ProofRequest}; -use std::path::Path; -use std::time::Duration; -use store::localdb::LocalDB; -use store::{ProofState, WrapperProof}; -use tokio::task::JoinHandle; -use tokio_util::sync::CancellationToken; -use zkm_sdk::HashableKey; - -#[tracing::instrument(level = "info", skip(local_db))] -pub(crate) async fn create_missing_wrapper_tasks( - local_db: &LocalDB, - genesis_sequencer_commit_txid: &str, -) -> anyhow::Result { - let mut storage_processor = local_db.acquire().await?; - let operator_proofs = storage_processor.find_proven_operator_proofs_without_wrapper(32).await?; - let mut created = 0u64; - - for operator_proof in operator_proofs { - let Some(operator_path_to_proof) = operator_proof.path_to_proof.clone() else { - continue; - }; - let operator_vk_hash = - read_utf8_sidecar(&format!("{operator_path_to_proof}.vk_hash.bin")).unwrap_or_default(); - let now = current_time_secs(); - let wrapper_proof = WrapperProof { - operator_proof_id: operator_proof.id, - instance_id: operator_proof.instance_id, - graph_id: operator_proof.graph_id, - execution_layer_block_number: operator_proof.execution_layer_block_number, - operator_path_to_proof, - operator_vk_hash, - genesis_sequencer_commit_txid: genesis_sequencer_commit_txid.to_string(), - operator_public_value_hex: operator_proof.public_value_hex.clone(), - proof_state: ProofState::New.to_i64(), - created_at: now, - updated_at: now, - ..Default::default() - }; - - match storage_processor.create_wrapper_proof(&wrapper_proof).await { - Ok(rows) => created += rows, - Err(err) if err.to_string().contains("UNIQUE") => { - tracing::debug!( - operator_proof_id = operator_proof.id, - "wrapper task already exists" - ); - } - Err(err) => return Err(err), - } - } - - Ok(created) -} - -#[tracing::instrument(level = "info", skip(local_db, cancellation_token))] -pub(crate) fn spawn_wrapper_proof_task( - args: operator_wrapper_proof::Args, - local_db: LocalDB, - interval: u64, - initial_delay: u64, - cancellation_token: CancellationToken, -) -> JoinHandle> { - let args = args.clone(); - tokio::spawn(async move { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(initial_delay)) => {} - _ = cancellation_token.cancelled() => { - anyhow::bail!("Wrapper proof generate task cancelled"); - } - } - - let builder = OperatorWrapperProofBuilder::new(); - tracing::info!("operator wrapper vk hash {:?}", builder.vk().bytes32()); - let scan_interval = if args.scan_interval == 0 { interval } else { args.scan_interval }; - loop { - tokio::select! { - _ = tokio::time::sleep(Duration::from_secs(scan_interval)) => { - if args.genesis_sequencer_commit_txid.is_empty() { - tracing::warn!("Wrapper proof task skipped: genesis_sequencer_commit_txid is empty"); - continue; - } - - let created = create_missing_wrapper_tasks( - &local_db, - &args.genesis_sequencer_commit_txid, - ).await?; - if created > 0 { - tracing::info!("created {created} wrapper proof tasks"); - } - - let wrapper_task = { - let mut storage_processor = local_db.acquire().await?; - storage_processor - .claim_next_wrapper_proof() - .await? - }; - let Some(wrapper_task) = wrapper_task else { - tracing::debug!("No wrapper proof task found"); - continue; - }; - - let output = wrapper_output_path(&args.output, &wrapper_task); - let ctx = ProofRequest::WrapperProofRequest { - operator_proof_id: wrapper_task.operator_proof_id, - operator_input_proof: wrapper_task.operator_path_to_proof.clone(), - graph_id: *wrapper_task.graph_id.as_bytes(), - genesis_sequencer_commit_txid: wrapper_task.genesis_sequencer_commit_txid.clone(), - output: output.clone(), - }; - - let proving_start = tokio::time::Instant::now(); - let result = builder.build_proof(&ctx).and_then(|(input, proof, cycles, proving_time)| { - let zkm_version = proof.zkm_version.clone(); - let (public_value_hex, proof_size) = - builder.save_proof(&ctx, &input, cycles, proof)?; - Ok((cycles, proving_time, public_value_hex, proof_size, zkm_version)) - }); - - match result { - Ok((cycles, proving_time, public_value_hex, proof_size, zkm_version)) => { - let proving_duration = proving_start.elapsed().as_secs_f32() * 1000.0; - let mut storage_processor = local_db.acquire().await?; - let affected = storage_processor - .update_wrapper_proof_success( - wrapper_task.id, - output, - public_value_hex, - proof_size as i64, - cycles as i64, - proving_time as i64, - &zkm_version, - ) - .await?; - tracing::info!( - wrapper_proof_id = wrapper_task.id, - operator_proof_id = wrapper_task.operator_proof_id, - proving_duration_ms = proving_duration as i64, - affected, - "wrapper proof generated" - ); - } - Err(err) => { - let mut storage_processor = local_db.acquire().await?; - storage_processor - .update_wrapper_proof_failure(wrapper_task.id) - .await?; - tracing::warn!( - wrapper_proof_id = wrapper_task.id, - operator_proof_id = wrapper_task.operator_proof_id, - error = %err, - "wrapper proof generation failed" - ); - } - } - } - _ = cancellation_token.cancelled() => { - anyhow::bail!("Wrapper proof generate task cancelled"); - } - } - } - }) -} - -fn wrapper_output_path(output: &str, wrapper_task: &WrapperProof) -> String { - let path = Path::new(output); - let output_dir = path - .parent() - .filter(|parent| !parent.as_os_str().is_empty()) - .unwrap_or_else(|| Path::new(".")); - output_dir - .join(format!( - "{}-{}.bin", - wrapper_task.graph_id.as_simple(), - wrapper_task.operator_proof_id - )) - .to_string_lossy() - .to_string() -} - -fn read_utf8_sidecar(path: &str) -> anyhow::Result { - Ok(String::from_utf8(std::fs::read(path)?)?) -} - -#[cfg(test)] -mod tests { - use super::*; - use store::{OperatorProof, create_local_db}; - use uuid::Uuid; - - #[tokio::test] - async fn create_missing_wrapper_tasks_is_unique_and_only_consumes_proven_operators() { - let local_db = create_local_db("sqlite::memory:").await; - let instance_id = Uuid::parse_str("00112233445566778899aabbccddeeff").unwrap(); - let graph_id = Uuid::parse_str("ffeeddccbbaa99887766554433221100").unwrap(); - let new_graph_id = Uuid::parse_str("11111111111111111111111111111111").unwrap(); - let now = current_time_secs(); - - { - let mut storage_processor = local_db.acquire().await.unwrap(); - storage_processor - .create_operator_proof(&OperatorProof { - instance_id, - graph_id, - execution_layer_block_number: 9511055, - path_to_proof: Some("operator-proof.bin".to_string()), - public_value_hex: Some("operator-public".to_string()), - proof_state: ProofState::Proven.to_i64(), - created_at: now, - updated_at: now, - operator_committed_blockhash: - "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246" - .to_string(), - ..Default::default() - }) - .await - .unwrap(); - storage_processor - .create_operator_proof(&OperatorProof { - instance_id, - graph_id: new_graph_id, - execution_layer_block_number: 9511056, - path_to_proof: Some("operator-new.bin".to_string()), - proof_state: ProofState::New.to_i64(), - created_at: now, - updated_at: now, - operator_committed_blockhash: - "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246" - .to_string(), - ..Default::default() - }) - .await - .unwrap(); - } - - let genesis_txid = "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246"; - assert_eq!(create_missing_wrapper_tasks(&local_db, genesis_txid).await.unwrap(), 1); - assert_eq!(create_missing_wrapper_tasks(&local_db, genesis_txid).await.unwrap(), 0); - - let mut storage_processor = local_db.acquire().await.unwrap(); - let wrapper_proof = storage_processor - .find_wrapper_proof_by_instance_and_graph(&instance_id, &graph_id) - .await - .unwrap() - .expect("wrapper proof should exist"); - assert_eq!(wrapper_proof.operator_path_to_proof, "operator-proof.bin"); - assert_eq!(wrapper_proof.operator_public_value_hex.as_deref(), Some("operator-public")); - assert_eq!(wrapper_proof.proof_state, ProofState::New.to_i64()); - assert!( - storage_processor - .find_wrapper_proof_by_instance_and_graph(&instance_id, &new_graph_id) - .await - .unwrap() - .is_none() - ); - } -} From 8d22085810cd467c99e45b73b37364fa7b0f9c70 Mon Sep 17 00:00:00 2001 From: Blake Hau <0xblake.sg@gmail.com> Date: Wed, 24 Jun 2026 21:07:26 +0800 Subject: [PATCH 05/14] bind graph watchtower public keys at runtime in operator proofs (#408) --- circuits/operator-proof/guest/Cargo.lock | 1 - circuits/operator-proof/guest/Cargo.toml | 4 - circuits/operator-proof/guest/build.rs | 68 --------- circuits/operator-proof/guest/src/main.rs | 7 +- circuits/operator-proof/host/build.rs | 8 - circuits/operator-proof/host/src/lib.rs | 58 +++++-- circuits/operator-proof/host/src/main.rs | 4 + circuits/proof-builder/src/lib.rs | 2 + .../bitcoin-light-client-circuit/src/lib.rs | 126 +++++++++++++--- node/Cargo.toml | 4 - .../src/bin/fetch_watchtower_xonly_pubkeys.rs | 108 -------------- node/src/handle.rs | 8 +- node/src/utils.rs | 141 ++++++++++++++---- proof-builder-rpc/src/task/operator_proof.rs | 21 +-- 14 files changed, 281 insertions(+), 279 deletions(-) delete mode 100644 circuits/operator-proof/guest/build.rs delete mode 100644 node/src/bin/fetch_watchtower_xonly_pubkeys.rs diff --git a/circuits/operator-proof/guest/Cargo.lock b/circuits/operator-proof/guest/Cargo.lock index f09ea595..91821b9a 100644 --- a/circuits/operator-proof/guest/Cargo.lock +++ b/circuits/operator-proof/guest/Cargo.lock @@ -2273,7 +2273,6 @@ dependencies = [ "bitcoin-light-client-circuit", "commit-chain", "header-chain", - "hex", "sha2 0.10.9", "state-chain", "tracing", diff --git a/circuits/operator-proof/guest/Cargo.toml b/circuits/operator-proof/guest/Cargo.toml index a3be8844..f3ff6ba3 100644 --- a/circuits/operator-proof/guest/Cargo.toml +++ b/circuits/operator-proof/guest/Cargo.toml @@ -26,10 +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" -[build-dependencies] -hex = "0.4.3" - - [patch.crates-io] # Precompile patches sha2 = { git = "https://github.com/ziren-patches/RustCrypto-hashes", branch = "patch-sha2-0.10.9", package = "sha2" } diff --git a/circuits/operator-proof/guest/build.rs b/circuits/operator-proof/guest/build.rs deleted file mode 100644 index 6263faaf..00000000 --- a/circuits/operator-proof/guest/build.rs +++ /dev/null @@ -1,68 +0,0 @@ -use std::{env, fs, path::PathBuf}; - -const ENV_FIXED_WATCHTOWER_KEYS: &str = "FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS"; - -fn parse_xonly_key(raw: &str) -> Result<[u8; 32], String> { - let hex = raw.trim().strip_prefix("0x").unwrap_or(raw.trim()); - let bytes = hex::decode(hex).map_err(|err| format!("invalid x-only public key hex: {err}"))?; - bytes.try_into().map_err(|bytes: Vec| { - format!("x-only public key must be 32 bytes, got {}", bytes.len()) - }) -} - -fn fixed_watchtower_keys_from_env() -> Vec<[u8; 32]> { - let value = match env::var(ENV_FIXED_WATCHTOWER_KEYS) { - Ok(value) => value, - Err(_) => { - println!( - "cargo:warning={ENV_FIXED_WATCHTOWER_KEYS} is not set; building operator guest with an empty fixed watchtower list" - ); - return Vec::new(); - } - }; - let keys = value - .split(',') - .map(str::trim) - .filter(|key| !key.is_empty()) - .map(parse_xonly_key) - .collect::, _>>() - .unwrap_or_else(|err| panic!("invalid {ENV_FIXED_WATCHTOWER_KEYS}: {err}")); - - if keys.is_empty() { - println!( - "cargo:warning={ENV_FIXED_WATCHTOWER_KEYS} contains no keys; building operator guest with an empty fixed watchtower list" - ); - return Vec::new(); - } - if keys.len() > 256 { - panic!("{ENV_FIXED_WATCHTOWER_KEYS} contains {} keys, max 256", keys.len()); - } - keys -} - -fn main() { - println!("cargo:rerun-if-env-changed={ENV_FIXED_WATCHTOWER_KEYS}"); - - let keys = fixed_watchtower_keys_from_env(); - let mut generated = String::new(); - generated.push_str(&format!("pub const FIXED_WATCHTOWER_COUNT: usize = {};\n", keys.len())); - generated.push_str(&format!( - "pub const FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS: [[u8; 32]; {}] = [\n", - keys.len() - )); - for key in keys { - generated.push_str(" ["); - for (index, byte) in key.iter().enumerate() { - if index > 0 { - generated.push_str(", "); - } - generated.push_str(&format!("0x{byte:02x}")); - } - generated.push_str("],\n"); - } - generated.push_str("];\n"); - - let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR is set by cargo")); - fs::write(out_dir.join("fixed_watchtowers.rs"), generated) - .expect("failed to write fixed_watchtowers.rs"); -} diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index 3563f489..7431c5a4 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -8,8 +8,6 @@ use header_chain::{HeaderChainCircuitInput, SPV}; use state_chain::StateChainCircuitInput; use std::str::FromStr; -include!(concat!(env!("OUT_DIR"), "/fixed_watchtowers.rs")); - pub fn main() { // calculate operator public input: https://github.com/ProjectZKM/Ziren/blob/main/crates/sdk/src/utils.rs#L42 let included_watchtowers: U256 = zkm_zkvm::io::read::(); @@ -20,6 +18,8 @@ pub fn main() { let latest_sequencer_commit_txid = operator_latest_sequencer_commit_txn.compute_txid(); // public input // https://github.com/KSlashh/BitVM/blob/v2/goat/src/transactions/watchtower_challenge.rs#L128 + let watchtower_challenge_indices: Vec = zkm_zkvm::io::read(); + let graph_watchtower_xonly_public_keys: Vec<[u8; 32]> = zkm_zkvm::io::read(); 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(); @@ -36,11 +36,12 @@ pub fn main() { included_watchtowers, graph_id, operator_genesis_sequencer_commit_txid, + watchtower_challenge_indices, watchtower_challenge_txns, watchtower_challenge_txn_pubkey, watchtower_challenge_txn_scripts, watchtower_challenge_txn_prev_outs, - &FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS, + &graph_watchtower_xonly_public_keys, operator_header_chain, operator_commit_chain, operator_state_chain, diff --git a/circuits/operator-proof/host/build.rs b/circuits/operator-proof/host/build.rs index bd89ff46..dcfa0c53 100644 --- a/circuits/operator-proof/host/build.rs +++ b/circuits/operator-proof/host/build.rs @@ -1,13 +1,5 @@ use zkm_build::build_program; -const ENV_FIXED_WATCHTOWER_KEYS: &str = "FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS"; - fn main() { - println!("cargo:rerun-if-env-changed={ENV_FIXED_WATCHTOWER_KEYS}"); - if std::env::var(ENV_FIXED_WATCHTOWER_KEYS).is_err() { - println!( - "cargo:warning={ENV_FIXED_WATCHTOWER_KEYS} is not set; building operator guest with an empty fixed watchtower list" - ); - } build_program("../guest"); } diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 2f37d1dd..9c006875 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -89,6 +89,34 @@ use sha2::{Digest, Sha256}; use std::sync::OnceLock; static ELF_ID: OnceLock = OnceLock::new(); +/// 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<(Vec<(u16, Txid, PublicKey)>, Vec<[u8; 32]>)> { + let txids = watchtower_challenge_txids.split(',').collect::>(); + let public_keys = watchtower_public_keys + .split(',') + .map(PublicKey::from_str) + .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)?, public_keys[index]))) + .collect::>>()?; + + Ok((included, graph_keys)) +} + pub async fn fetch_target_block_and_watchtower_tx( esplora_url: &str, latest_sequencer_commit_txid: &str, @@ -102,19 +130,15 @@ pub async fn fetch_target_block_and_watchtower_tx( bitcoin::Block, BlockHash, Transaction, + Vec, + Vec<[u8; 32]>, Vec, Vec, Vec, Vec, )> { - let watchtower_challenge_txids: Vec<&str> = - watchtower_challenge_txids.split(",").filter(|s| !s.is_empty()).collect(); - let watchtower_public_keys: Vec<&str> = - watchtower_public_keys.split(",").filter(|s| !s.is_empty()).collect(); - anyhow::ensure!( - watchtower_challenge_txids.len() == watchtower_public_keys.len(), - "watchtower challenge txids and public keys must have equal lengths" - ); + let (indexed_watchtower_inputs, graph_watchtower_xonly_public_keys) = + parse_indexed_watchtower_inputs(watchtower_challenge_txids, watchtower_public_keys)?; let btc_client = BTCClient::new(bitcoin_network, Some(&esplora_url)); let latest_sequencer_commit_txid = Txid::from_str(&latest_sequencer_commit_txid)?; @@ -166,6 +190,7 @@ pub async fn fetch_target_block_and_watchtower_tx( // --- watchtower_challenge_txns --- // let mut watchtower_challenge_txns = Vec::new(); + let mut watchtower_challenge_indices = 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(); @@ -179,12 +204,11 @@ pub async fn fetch_target_block_and_watchtower_tx( ), }; - 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, public_key) in indexed_watchtower_inputs { + tracing::info!("txid: {}, pk: {}", txid, public_key); 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 @@ -192,8 +216,8 @@ pub async fn fetch_target_block_and_watchtower_tx( 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_indices.push(node_index); + watchtower_challenge_txn_pubkeys.push(public_key); watchtower_challenge_txns.push(txn); // https://github.com/GOATNetwork/BitVM/blob/GA/goat/src/transactions/watchtower_challenge.rs#L45 @@ -214,6 +238,8 @@ pub async fn fetch_target_block_and_watchtower_tx( target_block_ss_commit, operator_committed_blockhash, operator_latest_sequencer_commit_txn, + watchtower_challenge_indices, + graph_watchtower_xonly_public_keys, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, @@ -271,6 +297,8 @@ impl ProofBuilder for OperatorProofBuilder { operator_committed_blockhash, + watchtower_challenge_indices, + graph_watchtower_xonly_public_keys, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, @@ -413,6 +441,8 @@ impl ProofBuilder for OperatorProofBuilder { stdin.write(&operator_genesis_sequencer_commit_txid.to_byte_array()); stdin.write(&operator_latest_sequencer_commit_txn); + stdin.write(&watchtower_challenge_indices); + stdin.write(&graph_watchtower_xonly_public_keys); stdin.write(&watchtower_challenge_txns); stdin.write(&watchtower_challenge_txn_pubkeys); stdin.write(&watchtower_challenge_txn_scripts); diff --git a/circuits/operator-proof/host/src/main.rs b/circuits/operator-proof/host/src/main.rs index 145bca9f..9101b09e 100644 --- a/circuits/operator-proof/host/src/main.rs +++ b/circuits/operator-proof/host/src/main.rs @@ -16,6 +16,8 @@ async fn main() { target_block_ss_commit, operator_committed_blockhash, operator_latest_sequencer_commit_txn, + watchtower_challenge_indices, + graph_watchtower_xonly_public_keys, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, @@ -51,6 +53,8 @@ async fn main() { operator_latest_sequencer_commit_txn, operator_committed_blockhash, + watchtower_challenge_indices, + graph_watchtower_xonly_public_keys, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, diff --git a/circuits/proof-builder/src/lib.rs b/circuits/proof-builder/src/lib.rs index ad3081a2..d90bc5ae 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -63,6 +63,8 @@ pub enum ProofRequest { operator_committed_blockhash: BlockHash, + watchtower_challenge_indices: Vec, + graph_watchtower_xonly_public_keys: Vec<[u8; 32]>, watchtower_challenge_txns: Vec, watchtower_challenge_txn_prev_outs: Vec, watchtower_challenge_txn_pubkeys: Vec, diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index df14b446..9cb38b9f 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -174,14 +174,14 @@ pub fn le_bits_to_u256(bits: &[bool]) -> U256 { u } -pub fn verify_fixed_watchtower_pubkey( - fixed_watchtower_xonly_public_keys: &[[u8; 32]], +pub fn verify_graph_watchtower_pubkey( + graph_watchtower_xonly_public_keys: &[[u8; 32]], index: usize, pubkey: &PublicKey, ) -> Result<(), String> { - // The fixed list is order-sensitive: index must match graph watchtower_pubkeys/node_index. - let Some(expected) = fixed_watchtower_xonly_public_keys.get(index) else { - return Err(format!("watchtower index {index} exceeds fixed watchtower list")); + // The graph list is order-sensitive: index must match graph watchtower_pubkeys/node_index. + let Some(expected) = graph_watchtower_xonly_public_keys.get(index) else { + return Err(format!("watchtower index {index} exceeds graph watchtower list")); }; let xonly: XOnlyPublicKey = (*pubkey).into(); let actual = xonly.serialize(); @@ -195,6 +195,37 @@ pub fn verify_fixed_watchtower_pubkey( Ok(()) } +/// 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], + challenge_count: usize, +) -> Result<(), String> { + if graph_watchtower_count == 0 || graph_watchtower_count > 256 { + return Err(format!("invalid graph watchtower count {graph_watchtower_count}")); + } + if challenge_indices.len() != challenge_count { + return Err("watchtower challenge index count mismatch".to_string()); + } + + 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(()) +} + // 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( @@ -202,11 +233,12 @@ pub fn propose_longest_chain( graph_id: [u8; GRAPH_ID_SIZE], // pis operator_genesis_sequencer_commit_txid: [u8; 32], // pis + watchtower_challenge_indices: Vec, watchtower_challenge_txns: Vec, watchtower_challenge_txn_pubkey: Vec, watchtower_challenge_txn_scripts: Vec, watchtower_challenge_txn_prev_outs: Vec, - fixed_watchtower_xonly_public_keys: &[[u8; 32]], + graph_watchtower_xonly_public_keys: &[[u8; 32]], operator_header_chain: HeaderChainCircuitInput, commit_chain: CommitChainCircuitInput, @@ -262,24 +294,34 @@ pub fn propose_longest_chain( // parse included_watchtowers into bits array let included_watchtowers_bits = u256_to_le_bits(included_watchtowers); println!("included watchtowers:{included_watchtowers_bits:?}"); + assert_eq!(watchtower_challenge_txns.len(), watchtower_challenge_txn_pubkey.len()); + assert_eq!(watchtower_challenge_txns.len(), watchtower_challenge_txn_scripts.len()); + assert_eq!(watchtower_challenge_txns.len(), watchtower_challenge_txn_prev_outs.len()); + validate_watchtower_challenge_indices( + &included_watchtowers_bits, + graph_watchtower_xonly_public_keys.len(), + &watchtower_challenge_indices, + watchtower_challenge_txns.len(), + ) + .expect("invalid indexed watchtower challenges"); - let mut valid_included_watchtower_count = 0usize; // 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() { + for (challenge_position, node_index) in watchtower_challenge_indices.iter().enumerate() { + let i = *node_index as usize; if included_watchtowers_bits[i] { - let tx = &watchtower_challenge_txns[i]; + let tx = &watchtower_challenge_txns[challenge_position]; println!("Verify watchtower[{i}] tx: {}, {:?}", tx.compute_txid(), tx); - let prev_out = &watchtower_challenge_txn_prev_outs[i]; + let prev_out = &watchtower_challenge_txn_prev_outs[challenge_position]; let prev_index = tx.input[0].previous_output.vout as usize; - let pubkey = &watchtower_challenge_txn_pubkey[i]; + let pubkey = &watchtower_challenge_txn_pubkey[challenge_position]; if let Err(err) = - verify_fixed_watchtower_pubkey(fixed_watchtower_xonly_public_keys, i, pubkey) + verify_graph_watchtower_pubkey(graph_watchtower_xonly_public_keys, i, pubkey) { - println!("Watchtower[{i}] fixed pubkey verification: {err}"); + println!("Watchtower[{i}] graph pubkey verification: {err}"); continue; } @@ -301,7 +343,7 @@ pub fn propose_longest_chain( }; // check tx signature is valid match verify_taproot_leaf_schnorr_signature( - &watchtower_challenge_txn_scripts[i], + &watchtower_challenge_txn_scripts[challenge_position], tx, prev_index, prev_out, @@ -375,11 +417,8 @@ pub fn propose_longest_chain( println!("Watchtower[{i}] consensus block height exceeds operator block height"); continue; } - - valid_included_watchtower_count += 1; } } - assert!(valid_included_watchtower_count > 0, "no included watchtower passed verification"); println!("verify el block"); verify_groth16_proof( @@ -436,7 +475,11 @@ 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, + graph_watchtower_xonly_public_keys, + ); println!("constant hex: {:?}", hex::encode(constant)); println!("btc_best_block_hash hex: {:?}", hex::encode(btc_best_block_hash)); @@ -458,10 +501,16 @@ pub fn propose_longest_chain( pub fn hash_operator_constant( graph_id: [u8; GRAPH_ID_SIZE], operator_genesis_sequencer_commit_txid: [u8; 32], + watchtower_xonly_public_keys: &[[u8; 32]], ) -> [u8; 32] { let mut engine = sha256::HashEngine::default(); + engine.input(b"bitvm2/operator-constant/v2"); engine.input(&graph_id); engine.input(&operator_genesis_sequencer_commit_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() } @@ -713,6 +762,47 @@ mod tests { ); } + #[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 mut input = b"bitvm2/operator-constant/v2".to_vec(); + input.extend_from_slice(&graph_id); + input.extend_from_slice(&genesis_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, &watchtower_keys), + *expected.as_byte_array() + ); + assert_ne!( + hash_operator_constant( + graph_id, + genesis_txid, + &[watchtower_keys[1], watchtower_keys[0]], + ), + *expected.as_byte_array() + ); + } + + #[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], 2).unwrap(); + assert!(validate_watchtower_challenge_indices(&included, 5, &[0, 1], 2).is_err()); + assert!(validate_watchtower_challenge_indices(&included, 5, &[1, 1], 2).is_err()); + } + #[test] fn test_u256_to_le_bits() { use std::str::FromStr; diff --git a/node/Cargo.toml b/node/Cargo.toml index fa4949db..f805d25a 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -35,10 +35,6 @@ path = "src/bin/send_bridge_out.rs" name = "update-db" path = "src/bin/db_inject.rs" -[[bin]] -name = "fetch-watchtower-xonly-pubkeys" -path = "src/bin/fetch_watchtower_xonly_pubkeys.rs" - [[bin]] name = "mock-rpc" path = "src/bin/mock_rpc.rs" diff --git a/node/src/bin/fetch_watchtower_xonly_pubkeys.rs b/node/src/bin/fetch_watchtower_xonly_pubkeys.rs deleted file mode 100644 index 3a67094a..00000000 --- a/node/src/bin/fetch_watchtower_xonly_pubkeys.rs +++ /dev/null @@ -1,108 +0,0 @@ -use alloy::primitives::Address as EvmAddress; -use anyhow::{Context, Result, anyhow}; -use bitvm_noded::env::{ - ENV_GOAT_CHAIN_URL, ENV_GOAT_GATEWAY_CONTRACT_ADDRESS, ENV_GOAT_NETWORK, - get_goat_gateway_contract_from_env, get_goat_network, goat_config_from_env, -}; -use client::goat_chain::GOATClient; -use serde::Serialize; -use sha2::{Digest, Sha256}; -use std::{ - env, fs, - path::{Path, PathBuf}, - time::{SystemTime, UNIX_EPOCH}, -}; - -#[derive(Debug, Serialize)] -struct WatchtowerSnapshot { - goat_chain_url: String, - goat_gateway_contract_address: String, - goat_network: String, - committee_management_address: String, - watchtower_xonly_public_keys: Vec, - watchtower_count: usize, - watchtower_list_hash: String, - watchtower_order_note: String, - generated_at_unix: u64, -} - -fn require_env(name: &str) -> Result { - env::var(name).map_err(|_| anyhow!("{name} must be set")) -} - -fn workspace_root() -> PathBuf { - Path::new(env!("CARGO_MANIFEST_DIR")) - .parent() - .expect("node crate is under the workspace root") - .to_path_buf() -} - -fn snapshot_path(gateway: &EvmAddress) -> PathBuf { - workspace_root() - .join("target") - .join("watchtower-snapshots") - .join(format!("{}.json", gateway.to_string().to_lowercase())) -} - -fn watchtower_list_hash(watchtower_pubkeys: &[String]) -> String { - let mut hasher = Sha256::new(); - // The hash is intentionally order-sensitive because watchtower index maps to node_index. - for key in watchtower_pubkeys { - hasher.update(key.as_bytes()); - } - format!("0x{}", hex::encode(hasher.finalize())) -} - -#[tokio::main] -async fn main() -> Result<()> { - dotenv::dotenv().ok(); - - let goat_chain_url = require_env(ENV_GOAT_CHAIN_URL)?; - let goat_gateway_contract_address = require_env(ENV_GOAT_GATEWAY_CONTRACT_ADDRESS)?; - let goat_network = env::var(ENV_GOAT_NETWORK).unwrap_or_else(|_| "test".to_string()); - - let gateway = get_goat_gateway_contract_from_env(); - let goat_client = GOATClient::new(goat_config_from_env().await, get_goat_network()); - let committee_management_address = - EvmAddress::from_slice(&goat_client.gateway_get_committee_management().await?); - // Preserve on-chain order. The operator circuit compares this list by index with graph - // watchtower_pubkeys, watchtower_challenge vouts, and included_watchtowers bitmap bits. - let watchtower_pubkeys = goat_client - .committee_mana_get_watchtowers() - .await? - .into_iter() - .map(|key| format!("0x{}", hex::encode(key.serialize()))) - .collect::>(); - - if watchtower_pubkeys.is_empty() { - return Err(anyhow!("committee management returned an empty watchtower list")); - } - - let watchtower_list_hash = watchtower_list_hash(&watchtower_pubkeys); - let snapshot = WatchtowerSnapshot { - goat_chain_url, - goat_gateway_contract_address, - goat_network, - committee_management_address: committee_management_address.to_string(), - watchtower_count: watchtower_pubkeys.len(), - watchtower_xonly_public_keys: watchtower_pubkeys.clone(), - watchtower_list_hash: watchtower_list_hash.clone(), - watchtower_order_note: - "order-sensitive: index must match graph watchtower_pubkeys/node_index".to_string(), - generated_at_unix: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(), - }; - - let path = snapshot_path(&gateway); - let parent = path.parent().context("snapshot path has parent")?; - fs::create_dir_all(parent)?; - fs::write(&path, serde_json::to_string_pretty(&snapshot)?)?; - - eprintln!("watchtower snapshot: {}", path.display()); - eprintln!("watchtower list hash: {watchtower_list_hash}"); - eprintln!( - "before build, export FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS or eval this command output" - ); - println!("export FIXED_WATCHTOWER_XONLY_PUBLIC_KEYS={}", watchtower_pubkeys.join(",")); - - Ok(()) -} diff --git a/node/src/handle.rs b/node/src/handle.rs index e16f75c9..1d278c44 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -1203,7 +1203,7 @@ async fn handle_init_graph_verifier( } else { get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE setup")?; - let static_input = derive_operator_statement(graph_id)?.static_input; + let static_input = derive_operator_static_input()?; let (setup_package, private_state) = tokio::task::spawn_blocking(move || { build_real_setup_package(BABE_N_CC, &vk, static_input) }) @@ -1402,7 +1402,7 @@ async fn handle_cut_circuits_verifier( get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE opening")?; - let static_input = derive_operator_statement(graph_id)?.static_input; + let static_input = derive_operator_static_input()?; let private_state = verifier_state.private_state.clone(); let selected_indices = selected_circuit_indexes.clone(); let package_for_opening = setup_package.clone(); @@ -1658,7 +1658,7 @@ async fn handle_compact_soldering_proof_operator( .context("expand compact soldering proof payload")?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE validation")?; - let static_input = derive_operator_statement(graph_id)?.static_input; + let static_input = derive_operator_static_input()?; let package_for_validation = setup_package.clone(); let opened_for_validation = opened.clone(); let finalized_for_validation = finalized.clone(); @@ -3935,7 +3935,7 @@ async fn handle_assert_sent_verifier( return Ok(()); } let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE challenge")?; - let static_input = derive_operator_statement(graph_id)?.static_input; + let static_input = derive_operator_static_input()?; let challenge_witness = build_real_challenge_assert_witness( &saved_verifier_state.private_state, &saved_verifier_state.setup_package, diff --git a/node/src/utils.rs b/node/src/utils.rs index 5a52ad2e..8a432807 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -163,6 +163,52 @@ pub mod todo_funcs { 1 } + /// Validates the graph's ordered watchtower selection against the contract registry and bound constant. + pub(super) fn validate_watchtower_selection( + selected: &[XOnlyPublicKey], + registered: &[XOnlyPublicKey], + graph_id: [u8; 16], + genesis_txid: [u8; 32], + constant: [u8; 32], + ) -> Result<()> { + use std::collections::HashSet; + + if selected.len() < min_required_watchtower() { + bail!(SpecialError::InvalidGraph(format!( + "insufficient watchtowers: have {}, required {}", + selected.len(), + min_required_watchtower() + ))); + } + if selected.len() > 256 { + bail!(SpecialError::InvalidGraph(format!( + "too many watchtowers: {}, max 256", + selected.len() + ))); + } + + let mut seen = HashSet::new(); + for key in selected { + if !seen.insert(*key) { + bail!(SpecialError::InvalidGraph( + "duplicate watchtower pubkey in graph".to_string() + )); + } + if !registered.contains(key) { + bail!(SpecialError::InvalidGraph(format!("watchtower {} is not registered", key))); + } + } + + let key_bytes = selected.iter().map(XOnlyPublicKey::serialize).collect::>(); + let expected = hash_operator_constant(graph_id, genesis_txid, &key_bytes); + if constant != expected { + bail!(SpecialError::InvalidGraph( + "operator constant mismatch for graph watchtower list".to_string() + )); + } + Ok(()) + } + pub async fn validate_init_graph( local_db: &LocalDB, btc_client: &BTCClient, @@ -207,31 +253,13 @@ pub mod todo_funcs { SpecialError::InvalidGraph(format!("failed to load watchtowers from chain: {e}")) })?; - // deduplicate watchtower pubkeys: reject graphs that contain duplicate watchtower entries - { - use std::collections::HashSet; - let mut seen = HashSet::new(); - for pk in &graph.parameters.watchtower_pubkeys { - if !seen.insert(*pk) { - bail!(SpecialError::InvalidGraph( - "duplicate watchtower pubkey in graph".to_string() - )); - } - } - } - // allow unregistered watchtowers as long as enough registered ones exist - let required = super::todo_funcs::min_required_watchtower(); - let valid_registered = graph - .parameters - .watchtower_pubkeys - .iter() - .filter(|pk| watchtowers_on_chain.contains(pk)) - .count(); - if valid_registered < required { - bail!(SpecialError::InvalidGraph(format!( - "insufficient registered watchtowers: have {valid_registered}, required {required}" - ))); - } + validate_watchtower_selection( + &graph.parameters.watchtower_pubkeys, + &watchtowers_on_chain, + *graph.parameters.graph_id.as_bytes(), + get_genesis_sequencer_commit_id(), + graph.parameters.pubin_disprove_constant, + )?; // 6) Operator stake sanity: verify operator is registered and has enough locked stake let op_pk_bytes = graph.parameters.operator_pubkey.to_bytes(); @@ -291,6 +319,18 @@ pub mod todo_funcs { bail!(SpecialError::InvalidGraph("unexpected challenge amount".to_string())); } + let watchtowers_on_chain = + goat_client.committee_mana_get_watchtowers().await.map_err(|e| { + SpecialError::InvalidGraph(format!("failed to load watchtowers from chain: {e}")) + })?; + validate_watchtower_selection( + &graph.parameters.watchtower_pubkeys, + &watchtowers_on_chain, + *graph.parameters.graph_id.as_bytes(), + get_genesis_sequencer_commit_id(), + graph.parameters.pubin_disprove_constant, + )?; + // 3) Validate endorsements: unique, from legitimate committee members, and signatures recover to the provided EVM address use std::collections::HashSet; let mut seen_committee: HashSet = HashSet::new(); @@ -2033,12 +2073,27 @@ fn combined_operator_vk_hash(operator_vk_hash: &str, zkm_version: &str) -> Resul Ok(encoded) } -pub fn derive_operator_statement(graph_id: Uuid) -> Result { +fn operator_identity() -> Result<([u8; 32], String, ark_bn254::Fr)> { let vk_hash = get_operator_vk_hash()?; let zkm_version = get_operator_zkm_version()?; - let combined_hash = combined_operator_vk_hash(&format!("0x{}", hex::encode(vk_hash)), &zkm_version)?; + let combined_hash = + combined_operator_vk_hash(&format!("0x{}", hex::encode(vk_hash)), &zkm_version)?; let static_input = load_ark_public_inputs_from_bytes(&combined_hash, &[0u8; 32])[0]; - let constant = hash_operator_constant(*graph_id.as_bytes(), get_genesis_sequencer_commit_id()); + Ok((vk_hash, zkm_version, static_input)) +} + +pub fn derive_operator_static_input() -> Result { + Ok(operator_identity()?.2) +} + +pub fn derive_operator_statement( + graph_id: Uuid, + watchtower_pubkeys: &[XOnlyPublicKey], +) -> Result { + let (vk_hash, zkm_version, static_input) = operator_identity()?; + let key_bytes = watchtower_pubkeys.iter().map(XOnlyPublicKey::serialize).collect::>(); + let constant = + hash_operator_constant(*graph_id.as_bytes(), get_genesis_sequencer_commit_id(), &key_bytes); Ok(OperatorStatement { static_input, vk_hash, zkm_version, constant }) } @@ -2123,7 +2178,11 @@ pub async fn get_operator_proof( return Ok((None, get_operator_proof_wait_secs())); }; - let statement = derive_operator_statement(graph_id)?; + let statement = + derive_operator_statement(graph_id, &bitvm_graph.parameters.watchtower_pubkeys)?; + if statement.constant != bitvm_graph.parameters.pubin_disprove_constant { + bail!("graph operator constant does not match its watchtower list"); + } let proof: ZKMProofWithPublicValues = bincode::deserialize(proof_data.proof.as_slice()) .map_err(|err| anyhow!("failed to deserialize operator proof: {err}"))?; @@ -2133,7 +2192,8 @@ pub async fn get_operator_proof( if operator_vk_hash_raw != statement.vk_hash { bail!("operator proof vk hash does not match configured operator identity"); } - if proof.zkm_version != statement.zkm_version || proof_data.zkm_version != statement.zkm_version { + if proof.zkm_version != statement.zkm_version || proof_data.zkm_version != statement.zkm_version + { bail!("operator proof Ziren version does not match configured operator identity"); } @@ -2810,7 +2870,17 @@ pub async fn build_graph_params( .to_byte_array() }) .collect(); - let pubin_disprove_constant = get_guest_constant_value(instance_id, graph_id)?; + let pubin_disprove_constant = + get_guest_constant_value(instance_id, graph_id, &watchtower_pubkeys)?; + // Local graph construction selects the full on-chain registry; passing the same list twice + // intentionally reuses the helper's size and uniqueness checks. + todo_funcs::validate_watchtower_selection( + &watchtower_pubkeys, + &watchtower_pubkeys, + *graph_id.as_bytes(), + get_genesis_sequencer_commit_id(), + pubin_disprove_constant, + )?; Ok(BitvmGcGraphParameters { instance_parameters, prekickoff_parameters, @@ -4532,8 +4602,13 @@ pub(super) async fn find_instances_by_escrow_hash<'a>( if size > 0 { Ok(Some(instances[0].clone())) } else { Ok(None) } } -pub fn get_guest_constant_value(_instance_id: Uuid, graph_id: Uuid) -> Result<[u8; 32]> { - Ok(hash_operator_constant(graph_id.into_bytes(), get_genesis_sequencer_commit_id())) +pub fn get_guest_constant_value( + _instance_id: Uuid, + graph_id: Uuid, + watchtower_pubkeys: &[XOnlyPublicKey], +) -> Result<[u8; 32]> { + let key_bytes = watchtower_pubkeys.iter().map(XOnlyPublicKey::serialize).collect::>(); + Ok(hash_operator_constant(graph_id.into_bytes(), get_genesis_sequencer_commit_id(), &key_bytes)) } pub(crate) async fn get_bridge_out_global_stats<'a>( storage_processor: &mut StorageProcessor<'a>, diff --git a/proof-builder-rpc/src/task/operator_proof.rs b/proof-builder-rpc/src/task/operator_proof.rs index bf42a9b2..3b9f87e4 100644 --- a/proof-builder-rpc/src/task/operator_proof.rs +++ b/proof-builder-rpc/src/task/operator_proof.rs @@ -52,24 +52,13 @@ pub(crate) fn spawn_operator_proof_task( args.graph_id ); args.watchtower_challenge_init_txid = next_task.watchtower_challenge_init_txid.unwrap().clone(); - let included_challenges: Vec<_> = next_task + args.watchtower_challenge_txids = next_task .watchtower_challenge_txids .iter() - .zip(&next_task.watchtower_public_keys) - .filter_map(|(txid, public_key)| { - txid.as_ref().map(|txid| (txid.as_str(), public_key.as_str())) - }) - .collect(); - args.watchtower_challenge_txids = included_challenges - .iter() - .map(|(txid, _)| *txid) - .collect::>() - .join(","); - args.watchtower_public_keys = included_challenges - .iter() - .map(|(_, public_key)| *public_key) + .map(|txid| txid.as_deref().unwrap_or("")) .collect::>() .join(","); + args.watchtower_public_keys = next_task.watchtower_public_keys.join(","); // LE array to string, e.g. [1, 1, 1, 0] => 7 args.included_watchtowers = le_bits_to_u256(&next_task.included_watchtowers).to_string(); task_index = next_task.task_index; @@ -92,6 +81,8 @@ pub(crate) fn spawn_operator_proof_task( target_block_ss_commit, operator_committed_blockhash, operator_latest_sequencer_commit_txn, + watchtower_challenge_indices, + graph_watchtower_xonly_public_keys, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, @@ -131,6 +122,8 @@ pub(crate) fn spawn_operator_proof_task( operator_committed_blockhash, + watchtower_challenge_indices, + graph_watchtower_xonly_public_keys, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, From 130763eeca3af25abe15e09e4b50a10f4e239ca0 Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Thu, 25 Jun 2026 21:55:04 +0800 Subject: [PATCH 06/14] fix some problem (#407) * fix musig2 problem * fix some problem * update timelock logic * add DisproveSent monitor * fix watchtower flow check logic * remove unused field/check * update deps --------- Co-authored-by: ethan --- Cargo.lock | 28 +- Cargo.toml | 4 +- crates/bitvm-gc/src/babe_adapter.rs | 120 ++- crates/bitvm-gc/src/committee/api.rs | 263 +++++- crates/bitvm-gc/src/keys.rs | 17 +- crates/bitvm-gc/src/lib.rs | 1 + crates/bitvm-gc/src/operator/api.rs | 20 +- crates/bitvm-gc/src/timelocks.rs | 157 ++++ crates/bitvm-gc/src/types.rs | 59 +- crates/bitvm-gc/src/verifier/api.rs | 14 +- crates/bitvm-gc/tests/babe_adapter.rs | 50 +- node/src/action.rs | 17 +- node/src/bin/send_pegin_request.rs | 11 +- node/src/env.rs | 17 +- node/src/handle.rs | 800 +++++++++++++----- .../graph_maintenance_tasks.rs | 196 +++-- .../instance_maintenance_tasks.rs | 10 +- node/src/utils.rs | 348 +++++++- 18 files changed, 1657 insertions(+), 475 deletions(-) create mode 100644 crates/bitvm-gc/src/timelocks.rs diff --git a/Cargo.lock b/Cargo.lock index 161e2f46..8e04f22b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1325,7 +1325,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -1336,7 +1336,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -3005,7 +3005,7 @@ dependencies = [ [[package]] name = "bitvm" version = "0.1.0" -source = "git+https://github.com/KSlashh/BitVM.git?branch=gc-v2#94193a6fb5d09c1b2419254c7dfcb2bd7b5bc0d2" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#db6adf333ca594400c698ec60ad3a1f37811e421" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -3046,7 +3046,7 @@ dependencies = [ "bincode", "bitcoin", "bitcoin-script", - "bitvm 0.1.0 (git+https://github.com/KSlashh/BitVM.git?branch=gc-v2)", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", "chacha20poly1305", "clap", "garbled-snark-verifier", @@ -3087,7 +3087,7 @@ dependencies = [ "bitcoin", "bitcoin-light-client-circuit", "bitcoin-script", - "bitvm 0.1.0 (git+https://github.com/KSlashh/BitVM.git?branch=gc-v2)", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", "bitvm-gc", "borsh", "cbft-rpc", @@ -4824,7 +4824,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "goat" version = "0.1.0" -source = "git+https://github.com/KSlashh/BitVM.git?branch=gc-v2#94193a6fb5d09c1b2419254c7dfcb2bd7b5bc0d2" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#db6adf333ca594400c698ec60ad3a1f37811e421" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -5752,7 +5752,7 @@ dependencies = [ "bitcoin", "bitcoin-script", "bitcoin-scriptexec", - "bitvm 0.1.0 (git+https://github.com/KSlashh/BitVM.git?branch=gc-v2)", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", "clap", "colored", "hex", @@ -8182,7 +8182,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11307,7 +11307,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -11410,7 +11410,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -12167,7 +12167,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -12853,7 +12853,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -14258,7 +14258,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 0870d287..f117e888 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,8 @@ members = [ default-members = ["node"] [workspace.dependencies] -bitvm = { git = "https://github.com/KSlashh/BitVM.git", branch = "gc-v2" } -goat = { git = "https://github.com/KSlashh/BitVM.git", branch = "gc-v2" } +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" diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 9dc093f0..61840a9c 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -2,9 +2,10 @@ 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::{Bn254, Fr}; use ark_bn254::g1::G1Affine; +use ark_bn254::{Bn254, Fr}; use ark_groth16::VerifyingKey as Groth16VerifyingKey; use ark_serialize::CanonicalSerialize; use garbled_snark_verifier::bag::S; @@ -16,18 +17,22 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use soldering_host::BabeBundle; pub use soldering_host::BabeBundleBuilder; -use verifiable_circuit_babe::babe::{WeKnownPi1SetupCt, GC_INPUT_WIRES, build_challenge_assert_witness_raw, deinterleave_dummy_positions, interleave_dummy_positions}; -pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; +use verifiable_circuit_babe::babe::{ + GC_INPUT_WIRES, WeKnownPi1SetupCt, build_challenge_assert_witness_raw, + deinterleave_dummy_positions, interleave_dummy_positions, +}; use verifiable_circuit_babe::cac::cac_finalize_indices; -use verifiable_circuit_babe::gc::{SparseAdaptorTable, SGC_PART1_CONSTANT_SIZE}; +pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; +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::TxAssertWitness; +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; -use crate::types::BitvmGcCircuitData; /// Number of Wots96 digit signatures expected by the GOAT GC-V2 connector. pub const WOTS_SIG_COUNT: usize = Wots96::TOTAL_DIGIT_LEN as usize; @@ -52,7 +57,6 @@ pub struct CompactSolderingProofPayload { pub soldering: CompactSolderingData, } - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SolderingData { pub finalized_indices: Vec, @@ -60,7 +64,6 @@ pub struct SolderingData { pub proof: Vec, } - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeVerifierPrivateState { pub instance_seeds: Vec, @@ -95,7 +98,6 @@ pub struct BabeWronglyChallengedWitness { 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) @@ -106,12 +108,7 @@ pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { .collect(); let constant_commit_sgc: Vec<_> = (0..SGC_PART1_CONSTANT_SIZE) - .map(|_| { - [ - hash32(&[seed, 0xf0, 0]), - hash32(&[seed, 0xf0, 1]), - ] - }) + .map(|_| [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])]) .collect(); CACInstanceCommit { @@ -130,18 +127,9 @@ pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { } pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { - let adaptor_tables = [ - SparseAdaptorTable { - entries: vec![] - }, - SparseAdaptorTable { - entries: vec![] - } - ]; - let ct_setup = WeKnownPi1SetupCt { - ct2_r_delta_g2: vec![], - ct3_masked_msg: vec![], - }; + let adaptor_tables = + [SparseAdaptorTable { entries: vec![] }, SparseAdaptorTable { entries: vec![] }]; + let ct_setup = WeKnownPi1SetupCt { ct2_r_delta_g2: vec![], ct3_masked_msg: vec![] }; FinalizedInstanceData { index, @@ -154,7 +142,6 @@ pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { } } - impl SolderingData { /// Builds deterministic placeholder soldering data for the selected finalized indices. pub fn sample(finalized_indices: Vec) -> Self { @@ -215,11 +202,7 @@ pub fn open_real_setup_and_solder( 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)?, - )) + Ok((bundle.opened, bundle.finalized, from_real_soldering(&bundle.soldering)?)) } /// Verifies real CAC openings, commitments, and the native Ziren soldering proof. @@ -327,7 +310,6 @@ pub fn verify_setup( if data.index >= n_cc { bail!("finalized index {} out of range", data.index); } - } Ok(()) } @@ -344,10 +326,8 @@ pub fn extract_gc_circuit_data( bail!("BABE epk has {} entries; expected {GC_INPUT_WIRES}", epk.len()); } let n = GC_INPUT_WIRES / 3; - let to_wire_hash = |pair: &[[u8; 20]; 2]| WireHash { - false_label_hash: pair[0], - true_label_hash: pair[1], - }; + let to_wire_hash = + |pair: &[[u8; 20]; 2]| WireHash { false_label_hash: pair[0], true_label_hash: pair[1] }; let dummy_hash = label_hash(&vec![0u8; 16]); let dummy = WireHash { false_label_hash: dummy_hash, true_label_hash: dummy_hash }; @@ -356,17 +336,11 @@ pub fn extract_gc_circuit_data( let x_d: Vec = epk[2 * n..].iter().map(to_wire_hash).collect(); let wire_hashes_vec = interleave_dummy_positions(&pi1_x, &pi1_y, &x_d, dummy); - let wire_hashes: [WireHash; INPUT_WIRE_NUM] = wire_hashes_vec.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, - }) + let wire_hashes: [WireHash; INPUT_WIRE_NUM] = + wire_hashes_vec.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. @@ -386,7 +360,7 @@ pub fn build_assert_witness( pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> { let recover = assert_witness.recover_pi1_xd_without_verify(); if recover.is_none() { - return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")); } let (pi1, x_d) = recover.unwrap(); Ok(pi1_xd_to_wots96_msg(&pi1, x_d)) @@ -409,7 +383,7 @@ pub fn build_challenge_assert_witness( .map(|index| hash16_with_index(&bytes, index)) .collect(), wots_sig: assert_witness.wots_sig.clone(), - } + }, }) } @@ -435,7 +409,7 @@ pub fn build_real_challenge_assert_witness( let recover = assert_witness.recover_pi1_xd_without_verify(); if recover.is_none() { - return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")); } let (pi1, x_d) = recover.unwrap(); let expected_message = pi1_xd_to_wots96_msg(&pi1, x_d); @@ -458,12 +432,12 @@ pub fn build_real_challenge_assert_witness( let witness = witness.unwrap(); if witness.input_labels.len() != INPUT_WIRE_NUM { - bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", witness.input_labels.len()); + bail!( + "real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", + witness.input_labels.len() + ); } - Ok(BabeChallengeAssertWitness { - verifier_index, - witness, - }) + Ok(BabeChallengeAssertWitness { verifier_index, witness }) } /// Builds a wrongly-challenged witness from a valid recovered finalized-message preimage. @@ -505,16 +479,19 @@ pub fn build_wrongly_challenged_witness_from_preimages( bail!("message is not a valid preimage"); } - Ok(BabeWronglyChallengedWitness { - verifier_index: challenge_witness.verifier_index, - final_msg, - }) + 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" + "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) @@ -540,9 +517,7 @@ fn restore_real_verifier( vk: &Groth16VerifyingKey, static_inputs: Fr, ) -> Result { - let verifier = BABEVerifier::from_state( - &state.instance_seeds, package, vk, static_inputs - ); + let verifier = BABEVerifier::from_state(&state.instance_seeds, package, vk, static_inputs); if verifier.is_none() { Err(anyhow::anyhow!("Cannot restore real verifier")) } else { @@ -619,7 +594,8 @@ fn recover_a_valid_finalized_messages( 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 base_input_labels: Vec = + challenge_witness.witness.input_labels.iter().map(|&b| S(b)).collect(); // Strip the 6 dummy labels before passing to the GC (which has GC_INPUT_WIRES real wires). let (pi1_x_labels, pi1_y_labels, x_d_labels) = deinterleave_dummy_positions(&base_input_labels); let pi1_labels: Vec = pi1_x_labels.into_iter().chain(pi1_y_labels).collect(); @@ -641,14 +617,18 @@ fn recover_a_valid_finalized_messages( &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(), + final_msg: prover + .valid_msg + .ok_or_else(|| { + anyhow::anyhow!("check_compute_msg returned true but valid_msg is not set") + })? + .to_vec(), }) } diff --git a/crates/bitvm-gc/src/committee/api.rs b/crates/bitvm-gc/src/committee/api.rs index 1911b0fc..d92b3b0e 100644 --- a/crates/bitvm-gc/src/committee/api.rs +++ b/crates/bitvm-gc/src/committee/api.rs @@ -1,16 +1,23 @@ use anyhow::{Result, bail, ensure}; use bitcoin::XOnlyPublicKey; -use bitcoin::{PublicKey, Transaction, key::Keypair, taproot::Signature as TaprootSignature}; +use bitcoin::{ + PublicKey, Script, TapLeafHash, TapSighash, TapSighashType, Transaction, TxOut, + key::Keypair, + sighash::{Prevouts, SighashCache}, + taproot::{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::{AggNonce, PartialSignature, PubNonce, SecNonce}; +use musig2::secp::Point; +use musig2::{AggNonce, KeyAggContext, PartialSignature, PubNonce, SecNonce, verify_partial}; use secp256k1::schnorr::Signature as SchnorrSignature; use serde::{Deserialize, Serialize}; use crate::keys::hkdf_derive_bytes; -use crate::types::BitvmGcGraph; +use crate::types::{BitvmGcGraph, BitvmGcInstanceParameters}; const COMMITTEE_NONCE_HKDF_SALT: &[u8] = b"bitvm-gc/committee-nonce/v1"; @@ -398,6 +405,256 @@ pub fn signature_aggregation( Ok(res) } +fn taproot_script_spend_sighash( + tx: &Transaction, + input_index: usize, + prevouts: &[TxOut], + leaf_hash: TapLeafHash, + sighash_type: TapSighashType, +) -> TapSighash { + 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, &prevouts[input_index]), + leaf_hash, + sighash_type, + ) + .expect("Failed to construct sighash") + } else { + SighashCache::new(tx) + .taproot_script_spend_signature_hash( + input_index, + &Prevouts::All(prevouts), + leaf_hash, + sighash_type, + ) + .expect("Failed to construct sighash") + } +} + +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}")) +} + +#[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_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, diff --git a/crates/bitvm-gc/src/keys.rs b/crates/bitvm-gc/src/keys.rs index 82f7fa0d..6da15bdd 100644 --- a/crates/bitvm-gc/src/keys.rs +++ b/crates/bitvm-gc/src/keys.rs @@ -338,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, 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); @@ -362,13 +364,18 @@ impl CommitteeMasterKey { ) } - 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) } diff --git a/crates/bitvm-gc/src/lib.rs b/crates/bitvm-gc/src/lib.rs index 3e8ed2ee..f0383511 100644 --- a/crates/bitvm-gc/src/lib.rs +++ b/crates/bitvm-gc/src/lib.rs @@ -7,6 +7,7 @@ pub mod actors; pub mod babe_adapter; pub mod keys; pub mod pegin; +pub mod timelocks; pub mod types; pub use goat::*; diff --git a/crates/bitvm-gc/src/operator/api.rs b/crates/bitvm-gc/src/operator/api.rs index 66ff9e66..9d11b9ff 100644 --- a/crates/bitvm-gc/src/operator/api.rs +++ b/crates/bitvm-gc/src/operator/api.rs @@ -5,7 +5,7 @@ use goat::assert_scripts::{ OperatorAssertPublicKey, OperatorAssertSecretKey, OperatorCommitPubinPublicKey, OperatorCommitPubinSecretKey, }; -use goat::constants::{CONNECTOR_A_TIMELOCK, CONNECTOR_D_TIMELOCK}; +use goat::constants::TimelockConfig; use goat::transactions::assert::{ DisproveTransaction, OperatorAssertTransaction, VerifierAssertTransaction, wrongly_challenged, }; @@ -24,10 +24,12 @@ use goat::transactions::watchtower_challenge::{ WatchtowerChallengeInitTransaction, WatchtowerChallengeTimeoutTransaction, operator_challenge_ack, operator_commit_pubin, }; -use goat::utils::num_blocks_per_network; use goat::wots::{Wots, Wots96}; 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"; @@ -66,6 +68,8 @@ pub fn operator_presig_num() -> usize { } 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 { @@ -589,9 +593,17 @@ pub fn operator_sign_wrongly_challenged( } pub fn take1_timelock(network: Network) -> u32 { - num_blocks_per_network(network, CONNECTOR_A_TIMELOCK) + 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 { - num_blocks_per_network(network, CONNECTOR_D_TIMELOCK) + 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/bitvm-gc/src/timelocks.rs b/crates/bitvm-gc/src/timelocks.rs new file mode 100644 index 00000000..9e611ae3 --- /dev/null +++ b/crates/bitvm-gc/src/timelocks.rs @@ -0,0 +1,157 @@ +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: 6, + connector_a: 6, + prover_connector: 12, + connector_d: 24, + watchtower_challenge: 24, + operator_ack: 36, + operator_commit: 48, + connector_f: 60, +}; +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: 1, + 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, + } +} + +pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Result<()> { + 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 == 0 { + bail!("timelock_config.{name} must be greater than 0"); + } + } + 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 + ); + } + + ensure_lte("prover_connector", config.prover_connector, "connector_d", config.connector_d)?; + ensure_lt( + "watchtower_challenge", + config.watchtower_challenge, + "operator_ack", + config.operator_ack, + )?; + ensure_lt("operator_ack", config.operator_ack, "operator_commit", config.operator_commit)?; + ensure_lt("operator_commit", config.operator_commit, "connector_f", config.connector_f)?; + + Ok(()) +} + +fn ensure_lte(left_name: &str, left: u32, right_name: &str, right: u32) -> Result<()> { + if left > right { + bail!("timelock_config.{left_name} must be <= timelock_config.{right_name}"); + } + Ok(()) +} + +fn ensure_lt(left_name: &str, left: u32, right_name: &str, right: u32) -> Result<()> { + if left >= right { + bail!("timelock_config.{left_name} must be < timelock_config.{right_name}"); + } + 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) +} diff --git a/crates/bitvm-gc/src/types.rs b/crates/bitvm-gc/src/types.rs index 8061c6fb..5a3fcfec 100644 --- a/crates/bitvm-gc/src/types.rs +++ b/crates/bitvm-gc/src/types.rs @@ -25,6 +25,7 @@ use goat::{ }, watchtower_connectors::{AckConnector, WatchtowerChallengeConnector}, }, + constants::TimelockConfig, contexts::{base::BaseContext, committee::CommitteeContext, operator::OperatorContext}, transactions::{ assert::{DisproveTransaction, OperatorAssertTransaction, VerifierAssertTransaction}, @@ -48,11 +49,13 @@ use goat::{ 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)] @@ -89,6 +92,7 @@ pub struct PrekickoffParameters { 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, @@ -131,6 +135,7 @@ impl BitvmGcInstanceParameters { self.network, &self.n_of_n_taproot_public_key(), &self.user_taproot_public_key(), + &crate::timelocks::default_timelock_config(self.network), ) } @@ -301,6 +306,7 @@ impl BitvmGcGraphParameters { self.network(), &self.operator_taproot_public_key(), &self.n_of_n_taproot_public_key(), + &self.timelock_config, ) } @@ -325,6 +331,7 @@ impl BitvmGcGraphParameters { &self.operator_assert_wots_pubkey, &self.pubin_disprove_constant, &self.watchtower_ack_hashlocks, + &self.timelock_config, ) } @@ -333,6 +340,7 @@ impl BitvmGcGraphParameters { self.network(), &self.n_of_n_taproot_public_key(), &self.operator_commit_pubin_wots_pubkey, + &self.timelock_config, ) } @@ -341,6 +349,7 @@ impl BitvmGcGraphParameters { self.network(), &self.operator_taproot_public_key(), &self.n_of_n_taproot_public_key(), + &self.timelock_config, ) } @@ -360,6 +369,7 @@ impl BitvmGcGraphParameters { self.network(), &self.operator_taproot_public_key(), watchtower_taproot_public_key, + &self.timelock_config, )) } @@ -371,6 +381,7 @@ impl BitvmGcGraphParameters { self.network(), &self.operator_taproot_public_key(), pubkey, + &self.timelock_config, ) }) .collect() @@ -381,14 +392,24 @@ impl BitvmGcGraphParameters { .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)) + 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) + AckConnector::new( + self.network(), + &self.n_of_n_taproot_public_key(), + *hashlock, + &self.timelock_config, + ) }) .collect() } @@ -429,6 +450,7 @@ impl BitvmGcGraphParameters { self.network(), self.n_of_n_taproot_public_key(), gc_data.final_msg_hashlocks.clone(), + &self.timelock_config, )) } @@ -440,11 +462,16 @@ impl BitvmGcGraphParameters { 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; @@ -668,6 +695,34 @@ impl BitvmGcGraph { } } +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.parameters_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 parameters_hash(&self) -> Result<[u8; 32]> { + let encoded = serde_json::to_vec(self)?; + Ok(Sha256::digest(encoded).into()) + } +} + pub struct BaseBitvmContext { pub network: Network, pub n_of_n_public_keys: Vec, diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 489d1915..c36d6d54 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -1,11 +1,14 @@ -use crate::types::BitvmGcGraph; +use crate::{ + timelocks::{default_timelock_config, disprove_timelock_blocks}, + types::BitvmGcGraph, +}; use anyhow::{Result, bail}; use bitcoin::{Address, Amount, Network, ScriptBuf, Transaction, TxIn, TxOut}; use bitvm::chunk::api::type_conversion_utils::RawWitness; use goat::{ assert_scripts::{INPUT_WIRE_NUM, Label}, connectors::{base::TaprootConnector, connector_d::CONNECTOR_D_PUBIN_DISPROVE_LEAF_INDEX}, - constants::PROVER_CONNECTOR_TIMELOCK, + constants::TimelockConfig, scripts::{generate_opreturn_script, p2a_output}, transactions::{ assert::{pubin_disprove, validate_pubin}, @@ -13,7 +16,6 @@ use goat::{ pre_signed::PreSignedTransaction, watchtower_challenge::extract_operator_preimage_from_ack_txin, }, - utils::num_blocks_per_network, }; /// challenge has a pre-signed SinglePlusAnyoneCanPay input and output @@ -234,5 +236,9 @@ pub fn build_pubin_disprove_txin( } pub fn disprove_timelock(network: Network) -> u32 { - num_blocks_per_network(network, PROVER_CONNECTOR_TIMELOCK) + 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) } diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index c8bf0798..3d1d809f 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -4,11 +4,12 @@ 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, verify_real_setup, verify_setup, - sample_cac_instance_commit, sample_finalized_instance_data + 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; @@ -92,8 +93,8 @@ fn babe_setup_payload_round_trips_and_derives_gc_data() { .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"); + 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); @@ -107,12 +108,11 @@ fn protocol_finalized_instances_contribute_one_base_wire_slot() { }; 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 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"); + 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); @@ -199,8 +199,8 @@ fn witness_builders_validate_inputs_and_indices() { }; let dynamic_input = Fr::from(0u64); - let assert_witness = build_assert_witness(&proof, &assert_secret_key, dynamic_input) - .expect("assert witness"); + 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.recover_pi1_xd_without_verify().is_some()); assert!(build_assert_witness(&proof, &Vec::new(), dynamic_input).is_err()); @@ -290,7 +290,7 @@ fn setup_package_and_open_reject_degenerate_inputs() { 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 + assert!(open_and_solder(&pkg, &[99]).is_err()); // out-of-range finalized index } #[test] @@ -311,17 +311,14 @@ fn witness_builders_reject_wrong_finalized_count() { 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()); + 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()); + assert!( + build_wrongly_challenged_witness_from_preimages(&too_many, &challenge, b"msg".to_vec(),) + .is_err() + ); } #[test] @@ -343,9 +340,8 @@ fn assert_witness_preserves_pi1_and_dynamic_input() { let assert_witness = build_assert_witness(&proof, &sk, dynamic_input).expect("assert witness"); - let (recovered_pi1, recovered_xd) = assert_witness - .recover_pi1_xd_without_verify() - .expect("recover pi1 and xd"); + let (recovered_pi1, recovered_xd) = + assert_witness.recover_pi1_xd_without_verify().expect("recover pi1 and xd"); assert_eq!(recovered_pi1, proof.a); assert_eq!(recovered_xd, dynamic_input); diff --git a/node/src/action.rs b/node/src/action.rs index 16b87a3d..b350f87a 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -11,10 +11,7 @@ use alloy::primitives::Address as EvmAddress; use anyhow::{Context, Result, anyhow}; use bitcoin::{PublicKey, Txid}; use bitvm_lib::actors::Actor; -use bitvm_lib::babe_adapter::{ - BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, TxAssertWitness, - CACSetupPackage, -}; +use bitvm_lib::babe_adapter::{BabeBundleBuilder, CACSetupPackage}; use bitvm_lib::committee::*; use bitvm_lib::types::{BitvmGcGraph, SimplifiedBitvmGcGraph}; use client::goat_chain::DisproveTxType; @@ -245,7 +242,6 @@ pub struct AssertSent { pub instance_id: Uuid, pub graph_id: Uuid, pub assert_txid: Txid, - pub assert_witness: Option, } #[derive(Serialize, Deserialize, Clone)] pub struct ChallengeAssertSent { @@ -253,7 +249,6 @@ pub struct ChallengeAssertSent { pub graph_id: Uuid, pub challenge_assert_txid: Txid, pub verifier_index: usize, - pub challenge_witness: Option, } #[derive(Serialize, Deserialize, Clone)] pub struct WronglyChallengeTimeout { @@ -261,7 +256,6 @@ pub struct WronglyChallengeTimeout { pub graph_id: Uuid, pub challenge_assert_txid: Txid, pub verifier_index: usize, - pub wrongly_challenged_witness: Option, } #[derive(Serialize, Deserialize, Clone)] pub struct DisproveSent { @@ -507,9 +501,14 @@ pub async fn try_finalize_graph( BitvmGcGraph::from_simplified(&g)? } }; - let pub_nonces = pub_nonoces.into_iter().map(|(_, pn)| pn).collect::>(); + let pub_nonces = + order_committee_values(&committee_pubkeys, pub_nonoces, "graph committee pub nonces")?; let agg_nonces = nonces_aggregation(&pub_nonces)?; - let partial_sigs = partial_sigs.into_iter().map(|(_, ps)| ps).collect::>(); + let partial_sigs = order_committee_values( + &committee_pubkeys, + partial_sigs, + "graph committee partial sigs", + )?; let committee_sig_for_graph = signature_aggregation(&partial_sigs, &agg_nonces, &graph)?; push_committee_pre_signatures(&mut graph, &committee_sig_for_graph)?; let simplified_graph = graph.to_simplified()?; diff --git a/node/src/bin/send_pegin_request.rs b/node/src/bin/send_pegin_request.rs index 354af0d0..641e1c99 100644 --- a/node/src/bin/send_pegin_request.rs +++ b/node/src/bin/send_pegin_request.rs @@ -25,7 +25,7 @@ use anyhow::{Result, anyhow}; use bitcoin::hashes::Hash; use bitcoin::sighash::EcdsaSighashType; use bitcoin::{Address, Amount, key::Keypair}; -use bitcoin::{Network, TapSighashType, XOnlyPublicKey}; +use bitcoin::{Network, TapSighashType}; use bitvm_lib::types::BitvmGcInstanceParameters; use clap::{Parser, Subcommand}; use client::btc_chain::BTCClient; @@ -33,7 +33,6 @@ use client::goat_chain::utils::get_gateway_relay_contracts; use client::goat_chain::{GOATClient, GoatInitConfig, GoatNetwork, Utxo as ClientUtxo}; use dotenv::dotenv; use goat::connectors::base::TaprootConnector; -use goat::connectors::connector_z::ConnectorZ; use goat::transactions::pre_signed::{PreSignedTransaction, pre_sign_taproot_input_default}; use reqwest::Url; use sha2::{Digest, Sha256}; @@ -231,7 +230,7 @@ async fn main() -> Result<()> { action_prepare(network, &btc_client, &goat_client, &uuid_str, &user_btc_secret).await } Commands::Cancel { instance_id, user_btc_secret } => { - action_cancel(network, &btc_client, &goat_client, &instance_id, &user_btc_secret).await + action_cancel(&btc_client, &goat_client, &instance_id, &user_btc_secret).await } } } @@ -373,7 +372,6 @@ async fn action_prepare( } async fn action_cancel( - network: Network, btc_client: &BTCClient, goat_client: &GOATClient, instance_id_str: &str, @@ -390,17 +388,14 @@ async fn action_cancel( &hex::encode(sk.secret_bytes()).to_string() }; let user_keypair = Keypair::from_seckey_str_global(user_btc_secret)?; - let user_taproot_public_key = user_keypair.public_key().x_only_public_key().0; let instance_params: BitvmGcInstanceParameters = bitvm_noded::utils::read_instance_info_from_goat(goat_client, instance_id).await?; - let n_of_n_taproot_public_key = XOnlyPublicKey::from(instance_params.committee_agg_pubkey); // Build pegin deposit/confirm/refund transactions and pick the refund (cancel) let (_deposit, _confirm, mut pegin_refund) = instance_params.build_pegin_tx()?; - let connector_z = - ConnectorZ::new(network, &n_of_n_taproot_public_key, &user_taproot_public_key); + let connector_z = instance_params.connector_z(); pre_sign_taproot_input_default( &mut pegin_refund, 0, diff --git a/node/src/env.rs b/node/src/env.rs index 092937b5..355a3c46 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -8,11 +8,11 @@ use base64::Engine; use bitcoin::{Network, PublicKey, key::Keypair}; use bitvm_lib::actors::Actor; use bitvm_lib::keys::NodeMasterKey; +use bitvm_lib::timelocks::{default_connector_z_timelock_blocks, estimated_block_interval_secs}; use client::goat_chain::utils::{ get_committee_management_contract, get_gateway_relay_contracts, is_validate_committee, }; use client::goat_chain::{GoatInitConfig, GoatNetwork}; -use goat::constants::{CONNECTOR_Z_TIMELOCK, NUM_BLOCKS_PER_HOUR}; use libp2p::PeerId; use reqwest::Url; use sha2::{Digest, Sha256}; @@ -130,10 +130,6 @@ pub const MESSAGE_SAVE_INTERVAL_SECOND: i64 = 3600 * 24 * 3; pub const GRAPH_OPERATOR_DATA_UPLOAD_TIME_EXPIRED: i64 = 3600 * 48; -// update me later -pub const INSTANCE_PRESIGNED_TIME_EXPIRED: i64 = - (3600 / NUM_BLOCKS_PER_HOUR as i64) * CONNECTOR_Z_TIMELOCK as i64 * 2 / 3; - pub const SYNC_GRAPH_INTERVAL: u64 = 3; pub const SYNC_GRAPH_MAX_WAIT_SECS: u64 = 30; @@ -171,6 +167,12 @@ pub fn get_network() -> Network { } } +pub fn get_instance_presigned_time_expired_secs() -> i64 { + let network = get_network(); + let connector_z_blocks = default_connector_z_timelock_blocks(network) as i64; + estimated_block_interval_secs(network) * connector_z_blocks * 2 / 3 +} + pub fn get_goat_network() -> GoatNetwork { let network = std::env::var(ENV_GOAT_NETWORK).unwrap_or("test".to_string()); match network.as_str() { @@ -624,9 +626,8 @@ pub struct BabeGcAssetPaths { pub fn get_babe_gc_asset_paths() -> anyhow::Result { fn read_path(name: &str) -> anyhow::Result { - let path = PathBuf::from( - std::env::var(name).map_err(|_| anyhow::anyhow!("{name} is missing"))?, - ); + let path = + PathBuf::from(std::env::var(name).map_err(|_| anyhow::anyhow!("{name} is missing"))?); if !path.is_file() { anyhow::bail!("{name} does not point to a readable file: {}", path.display()); } diff --git a/node/src/handle.rs b/node/src/handle.rs index 1d278c44..d7219c34 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -15,7 +15,14 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; -use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, TxAssertWitness, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage, WOTS_SIG_COUNT}; +use bitvm_lib::babe_adapter::{ + BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, + CACSetupPackage, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, TxAssertWitness, + WOTS_SIG_COUNT, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, + build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, + extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, + verify_real_setup, +}; use bitvm_lib::committee::*; use bitvm_lib::keys::*; use bitvm_lib::operator::*; @@ -24,7 +31,6 @@ use bitvm_lib::verifier::*; use client::goat_chain::{DisproveTxType, PeginStatus, WithdrawStatus}; use client::http_client::async_client::HttpAsyncClient; use client::{btc_chain::BTCClient, goat_chain::GOATClient}; -use goat::connectors::connector_z::ConnectorZ; use goat::transactions::base::output_topology; use goat::transactions::pre_signed::PreSignedTransaction; use goat::transactions::pre_signed_musig2::verify_public_nonce; @@ -32,8 +38,8 @@ use goat::wots::{Wots, Wots96}; use libp2p::gossipsub::MessageId; use libp2p::{PeerId, Swarm}; use std::sync::Arc; -use store::{GraphStatus, SerializableTxid}; use store::localdb::LocalDB; +use store::{GraphStatus, SerializableTxid}; use uuid::Uuid; pub struct HandlerContext<'a> { @@ -310,6 +316,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent received_committee_pubkey, committee_partial_sigs, agg_nonces, + content, ) .await } @@ -388,6 +395,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent received_committee_pubkey, partial_sig, endorse_sig, + content, ) .await } @@ -494,24 +502,15 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent Actor::Operator, ) => handle_assert_ready_operator(ctx, *instance_id, *graph_id).await, ( - GOATMessageContent::AssertSent(AssertSent { - instance_id, - graph_id, - assert_txid, - assert_witness, - }), + GOATMessageContent::AssertSent(AssertSent { instance_id, graph_id, assert_txid }), Actor::Verifier, - ) => { - handle_assert_sent_verifier(ctx, *instance_id, *graph_id, *assert_txid, assert_witness) - .await - } + ) => handle_assert_sent_verifier(ctx, *instance_id, *graph_id, *assert_txid, content).await, ( GOATMessageContent::ChallengeAssertSent(ChallengeAssertSent { instance_id, graph_id, challenge_assert_txid, verifier_index, - challenge_witness, .. }), Actor::Operator, @@ -522,7 +521,6 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent *graph_id, *challenge_assert_txid, *verifier_index, - challenge_witness, content, ) .await @@ -1653,9 +1651,8 @@ async fn handle_compact_soldering_proof_operator( bail!("selected verifier candidate index does not match SolderingProof slot"); } let setup_package = candidate.setup_package.clone(); - let (opened, finalized, soldering) = - expand_compact_soldering_proof_payload(payload) - .context("expand compact soldering proof payload")?; + let (opened, finalized, soldering) = expand_compact_soldering_proof_payload(payload) + .context("expand compact soldering proof payload")?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE validation")?; let static_input = derive_operator_static_input()?; @@ -1687,7 +1684,8 @@ async fn handle_compact_soldering_proof_operator( bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); } let epk = &setup_package.commits[finalized[0].index].epk; - let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| setup_package.commits[f.index].h_msg).collect(); + let h_msgs: Vec<[u8; 20]> = + finalized.iter().map(|f| setup_package.commits[f.index].h_msg).collect(); let gc_data = extract_gc_circuit_data(verifier_pubkey, epk, &h_msgs)?; let prover_state = BabeProverState { package: setup_package.clone(), @@ -1800,14 +1798,30 @@ async fn handle_create_graph_committee( let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; let verifier_num = graph.parameters.gc_data.len(); let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let (pub_nonces, _, nonce_sigs) = committee_master_key.nonces_for_graph_with_keypair( + let graph_parameters_hash = graph.parameters_hash()?; + let (pub_nonces, _, nonce_sigs) = committee_master_key.nonces_for_graph_job_with_keypair( instance_id, graph_id, + graph_parameters_hash, watchtower_num, verifier_num, instance_keypair, ); let local_committee_pubkey = instance_keypair.public_key().into(); + if get_committee_partial_sigs_for_graph_member( + ctx.local_db, + instance_id, + graph_id, + &local_committee_pubkey, + ) + .await? + .is_some() + { + tracing::warn!( + "Ignore CreateGraph for {instance_id}:{graph_id}: local committee partial signatures already exist" + ); + return Ok(()); + } let message_content = GOATMessageContent::NonceGeneration(NonceGeneration { instance_id, graph_id, @@ -1832,26 +1846,55 @@ async fn handle_create_graph_committee( let mut graph = BitvmGcGraph::from_simplified(graph)?; let verifier_num = graph.verifier_asserts.len(); let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let mut pub_nonces = Vec::with_capacity(pub_nonces_unchecked.len()); - for (pk, pn) in pub_nonces_unchecked.into_iter() { + let mut checked_pub_nonces = Vec::with_capacity(pub_nonces_unchecked.len()); + for (pk, pn) in pub_nonces_unchecked.iter() { if let Err(e) = pn.validate_length(watchtower_num, verifier_num) { tracing::warn!("PubNonces from {} has invalid length: {e}", pk.to_string()); return Ok(()); } - pub_nonces.push(pn); + checked_pub_nonces.push((*pk, pn.clone())); } + let pub_nonces = order_committee_values( + &committee_pubkeys, + checked_pub_nonces, + "graph committee pub nonces", + )?; let agg_nonces = nonces_aggregation(&pub_nonces)?; let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; - let (_, sec_nonces, _) = committee_master_key.nonces_for_graph_with_keypair( + let local_committee_pubkey = instance_keypair.public_key().into(); + if get_committee_partial_sigs_for_graph_member( + ctx.local_db, instance_id, graph_id, + &local_committee_pubkey, + ) + .await? + .is_some() + { + tracing::warn!( + "Skip committee pre-sign for {instance_id}:{graph_id}: local committee partial signatures already exist" + ); + return Ok(()); + } + let (_, sec_nonces, _) = committee_master_key.nonces_for_graph_job_with_keypair( + instance_id, + graph_id, + graph_parameters_hash, watchtower_num, verifier_num, instance_keypair, ); let committee_partial_sigs = committee_pre_sign(instance_keypair, sec_nonces, agg_nonces.clone(), &mut graph)?; + store_committee_partial_sigs_for_graph( + ctx.local_db, + instance_id, + graph_id, + local_committee_pubkey, + committee_partial_sigs.clone(), + ) + .await?; let message_content = GOATMessageContent::CommitteePresign(CommitteePresign { instance_id, graph_id, @@ -1935,21 +1978,42 @@ async fn handle_nonce_generation_committee( let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; let local_committee_pubkey = instance_keypair.public_key().into(); + if get_committee_partial_sigs_for_graph_member( + ctx.local_db, + instance_id, + graph_id, + &local_committee_pubkey, + ) + .await? + .is_some() + { + tracing::warn!( + "Skip committee pre-sign for {instance_id}:{graph_id}: local committee partial signatures already exist" + ); + return Ok(()); + } let mut graph = BitvmGcGraph::from_simplified(&graph)?; let verifier_num = graph.verifier_asserts.len(); let watchtower_num = graph.parameters.watchtower_pubkeys.len(); - let mut pub_nonces = Vec::with_capacity(pub_nonces_unchecked.len()); - for (pk, pn) in pub_nonces_unchecked.into_iter() { + let mut checked_pub_nonces = Vec::with_capacity(pub_nonces_unchecked.len()); + for (pk, pn) in pub_nonces_unchecked.iter() { if let Err(e) = pn.validate_length(watchtower_num, verifier_num) { tracing::warn!("PubNonces from {} has invalid length: {e}", pk.to_string()); return Ok(()); } - pub_nonces.push(pn); + checked_pub_nonces.push((*pk, pn.clone())); } + let pub_nonces = order_committee_values( + &committee_pubkeys, + checked_pub_nonces, + "graph committee pub nonces", + )?; let agg_nonces = nonces_aggregation(&pub_nonces)?; - let (_, sec_nonces, _) = committee_master_key.nonces_for_graph_with_keypair( + let graph_parameters_hash = graph.parameters.parameters_hash()?; + let (_, sec_nonces, _) = committee_master_key.nonces_for_graph_job_with_keypair( instance_id, graph_id, + graph_parameters_hash, watchtower_num, verifier_num, instance_keypair, @@ -1957,6 +2021,14 @@ async fn handle_nonce_generation_committee( // 4. if received enough valid committee partial sigs, endorse the graph let committee_partial_sigs = committee_pre_sign(instance_keypair, sec_nonces, agg_nonces.clone(), &mut graph)?; + store_committee_partial_sigs_for_graph( + ctx.local_db, + instance_id, + graph_id, + local_committee_pubkey, + committee_partial_sigs.clone(), + ) + .await?; let message_content = GOATMessageContent::CommitteePresign(CommitteePresign { instance_id, graph_id, @@ -1965,14 +2037,6 @@ async fn handle_nonce_generation_committee( agg_nonces, }); send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; - store_committee_partial_sigs_for_graph( - ctx.local_db, - instance_id, - graph_id, - local_committee_pubkey, - committee_partial_sigs, - ) - .await?; let committee_partial_sigs = get_committee_partial_sigs_for_graph(ctx.local_db, instance_id, graph_id) .await? @@ -2077,6 +2141,93 @@ async fn handle_nonce_generation_operator( Ok(()) } +async fn validate_committee_presign_for_graph( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + received_committee_pubkey: &PublicKey, + committee_partial_sigs: &CommitteePartialSignatures, + received_agg_nonces: &CommitteeAggNonces, + content: &GOATMessageContent, +) -> Result> { + let message = make_message(ctx, content); + let graph = match get_graph_or_defer( + ctx.swarm, + ctx.local_db, + ctx.goat_client, + instance_id, + graph_id, + &message, + ) + .await? + { + Some(g) => g, + None => return Ok(None), + }; + let graph = BitvmGcGraph::from_simplified(&graph)?; + let committee_pubkeys = ctx.goat_client.gateway_get_committee_pubkeys(&instance_id).await?; + let pub_nonces_unchecked = + get_committee_pub_nonces_for_graph(ctx.local_db, instance_id, graph_id).await?; + if pub_nonces_unchecked.len() != committee_pubkeys.len() { + push_local_unhandled_messages(ctx.local_db, graph_id, &message, 30).await?; + tracing::info!( + "Defer CommitteePresign for {instance_id}:{graph_id}: waiting for committee pub nonces" + ); + return Ok(None); + } + let received_pub_nonces = match pub_nonces_unchecked + .iter() + .find(|(pubkey, _)| pubkey == received_committee_pubkey) + .map(|(_, pub_nonces)| pub_nonces.clone()) + { + Some(pub_nonces) => pub_nonces, + None => { + tracing::warn!( + "Ignore CommitteePresign for {instance_id}:{graph_id} from {}: missing signer pub nonces", + received_committee_pubkey + ); + return Ok(None); + } + }; + let pub_nonces = match order_committee_values( + &committee_pubkeys, + pub_nonces_unchecked, + "graph committee pub nonces", + ) { + Ok(pub_nonces) => pub_nonces, + Err(e) => { + tracing::warn!( + "Ignore CommitteePresign for {instance_id}:{graph_id} from {}: invalid pub nonce set: {e}", + received_committee_pubkey + ); + return Ok(None); + } + }; + let agg_nonces = nonces_aggregation(&pub_nonces)?; + if &agg_nonces != received_agg_nonces { + tracing::warn!( + "Ignore CommitteePresign for {instance_id}:{graph_id} from {}: agg_nonces mismatch", + received_committee_pubkey + ); + return Ok(None); + } + if let Err(e) = verify_graph_committee_partial_sigs( + &graph, + &committee_pubkeys, + received_committee_pubkey, + &received_pub_nonces, + &agg_nonces, + committee_partial_sigs, + ) { + tracing::warn!( + "Ignore CommitteePresign for {instance_id}:{graph_id} from {}: invalid partial signatures: {e}", + received_committee_pubkey + ); + return Ok(None); + } + Ok(Some(graph)) +} + #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_committee_presign_committee( ctx: &mut HandlerContext<'_>, @@ -2099,8 +2250,21 @@ async fn handle_committee_presign_committee( { return Ok(()); } - // 1. save the committee partial sigs to local db - // TODO: validate the partial sigs + let graph = match validate_committee_presign_for_graph( + ctx, + instance_id, + graph_id, + received_committee_pubkey, + committee_partial_sigs, + _agg_nonces, + content, + ) + .await? + { + Some(graph) => graph, + None => return Ok(()), + }; + // 1. save the validated committee partial sigs to local db store_committee_partial_sigs_for_graph( ctx.local_db, instance_id, @@ -2118,21 +2282,6 @@ async fn handle_committee_presign_committee( .map(|(_, ps)| ps) .collect::>(); if committee_partial_sigs.len() == committee_pubkeys.len() { - let message = make_message(ctx, content); - let graph = match get_graph_or_defer( - ctx.swarm, - ctx.local_db, - ctx.goat_client, - instance_id, - graph_id, - &message, - ) - .await? - { - Some(g) => g, - None => return Ok(()), - }; - let graph = BitvmGcGraph::from_simplified(&graph)?; let committee_sig_for_graph = endorse_graph(ctx.goat_client, &graph).await?; let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; @@ -2159,6 +2308,7 @@ async fn handle_committee_presign_operator( received_committee_pubkey: &PublicKey, committee_partial_sigs: &CommitteePartialSignatures, _agg_nonces: &CommitteeAggNonces, + content: &GOATMessageContent, ) -> Result<()> { // received from Committee members if !ensure_self_or_valid_committee( @@ -2172,8 +2322,21 @@ async fn handle_committee_presign_operator( { return Ok(()); } - // 1. save the committee partial sigs to local db - // TODO: validate the partial sigs + if validate_committee_presign_for_graph( + ctx, + instance_id, + graph_id, + received_committee_pubkey, + committee_partial_sigs, + _agg_nonces, + content, + ) + .await? + .is_none() + { + return Ok(()); + } + // 1. save the validated committee partial sigs to local db store_committee_partial_sigs_for_graph( ctx.local_db, instance_id, @@ -2312,8 +2475,27 @@ async fn handle_graph_finalize_committee( ) .await?; if stored_pub_nonce.is_none() { - let (_, pub_nonce, nonce_sig) = - committee_master_key.nonce_for_instance_with_keypair(instance_id, instance_keypair); + if get_committee_partial_sig_for_instance( + ctx.local_db, + instance_id, + &local_committee_pubkey, + ) + .await? + .is_some() + { + tracing::warn!( + "Skip PeginConfirm nonce for {instance_id}: local committee partial signature already exists" + ); + return Ok(()); + } + let instance_parameters_hash = + graph.parameters.instance_parameters.parameters_hash()?; + let (_, pub_nonce, nonce_sig) = committee_master_key + .nonce_for_instance_job_with_keypair( + instance_id, + instance_parameters_hash, + instance_keypair, + ); let message_content = GOATMessageContent::PeginConfirmNonce(PeginConfirmNonce { instance_id, committee_pubkey: local_committee_pubkey, @@ -2425,13 +2607,31 @@ async fn handle_pegin_confirm_nonce_committee( let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; let local_committee_pubkey = instance_keypair.public_key().into(); - let (sec_nonce, _, _) = - committee_master_key.nonce_for_instance_with_keypair(instance_id, instance_keypair); - let agg_nonce = - nonce_aggregation(&pub_nonces.iter().map(|(_, pn)| pn.clone()).collect::>()); + if get_committee_partial_sig_for_instance( + ctx.local_db, + instance_id, + &local_committee_pubkey, + ) + .await? + .is_some() + { + tracing::warn!( + "Skip PeginConfirm partial signature for {instance_id}: local committee partial signature already exists" + ); + return Ok(()); + } + let pub_nonces = + order_committee_values(&committee_pubkeys, pub_nonces, "pegin committee pub nonces")?; + let agg_nonce = nonce_aggregation(&pub_nonces); let instance_params = get_instance_parameters(ctx.local_db, instance_id) .await? .ok_or_else(|| anyhow!("Instance parameters not found for {instance_id}"))?; + let instance_parameters_hash = instance_params.parameters_hash()?; + let (sec_nonce, _, _) = committee_master_key.nonce_for_instance_job_with_keypair( + instance_id, + instance_parameters_hash, + instance_keypair, + ); let mut pegin_confirm = instance_params.build_pegin_tx()?.1; let context = instance_params.get_committee_context(instance_keypair)?; let partial_sig = pegin_confirm @@ -2439,13 +2639,6 @@ async fn handle_pegin_confirm_nonce_committee( .map_err(|e| anyhow!("Failed to sign pegin confirm for {instance_id}: {e}"))?; let endorse_sig = endorse_pegin(ctx.goat_client, instance_id, &pegin_confirm.tx().compute_txid()).await?; - let message_content = GOATMessageContent::PeginConfirmPartialSig(PeginConfirmPartialSig { - instance_id, - committee_pubkey: local_committee_pubkey, - partial_sig, - endorse_sig: endorse_sig.as_bytes().to_vec(), - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::Committee, message_content)).await?; store_committee_partial_sig_for_instance( ctx.local_db, instance_id, @@ -2460,15 +2653,24 @@ async fn handle_pegin_confirm_nonce_committee( endorse_sig.as_bytes().to_vec(), ) .await?; + let message_content = GOATMessageContent::PeginConfirmPartialSig(PeginConfirmPartialSig { + instance_id, + committee_pubkey: local_committee_pubkey, + partial_sig, + endorse_sig: endorse_sig.as_bytes().to_vec(), + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Committee, message_content)).await?; // 4. (Relayer) if received enough partial signatures, aggregate the sigs if is_relayer() { - let partial_sigs = get_committee_partial_sigs_for_instance(ctx.local_db, instance_id) - .await? - .into_iter() - .map(|(_, ps)| ps) - .collect::>(); + let partial_sigs = + get_committee_partial_sigs_for_instance(ctx.local_db, instance_id).await?; let context = instance_params.get_base_context(); if partial_sigs.len() == committee_pubkeys.len() { + let partial_sigs = order_committee_values( + &committee_pubkeys, + partial_sigs, + "pegin committee partial sigs", + )?; let full_sig = pegin_confirm .aggregate_input_0_musig2_signatures(&context, partial_sigs, &agg_nonce) .map_err(|e| { @@ -2476,11 +2678,7 @@ async fn handle_pegin_confirm_nonce_committee( "Failed to aggregate Pegin-Confirm's signatures for {instance_id}: {e}" ) })?; - let connector_z = ConnectorZ::new( - context.network, - &context.n_of_n_taproot_public_key, - &instance_params.user_info.user_xonly_pubkey, - ); + let connector_z = instance_params.connector_z(); pegin_confirm.push_input_0_signature(&connector_z, full_sig); broadcast_tx(ctx.btc_client, pegin_confirm.tx()).await?; } @@ -2496,6 +2694,7 @@ async fn handle_pegin_confirm_partial_sig_committee( received_committee_pubkey: &PublicKey, partial_sig: &musig2::PartialSignature, endorse_sig: &[u8], + content: &GOATMessageContent, ) -> Result<()> { // received from Committee members if !ensure_self_or_valid_committee( @@ -2509,8 +2708,91 @@ async fn handle_pegin_confirm_partial_sig_committee( { return Ok(()); } - // 1. save the partial signature & endorsement signature to local db - // partial sigs will be validated when aggregating + let message = make_message(ctx, content); + let committee_pubkeys = ctx.goat_client.gateway_get_committee_pubkeys(&instance_id).await?; + let pub_nonces_unchecked = + get_committee_pub_nonces_for_instance(ctx.local_db, instance_id).await?; + if pub_nonces_unchecked.len() != committee_pubkeys.len() { + push_local_unhandled_messages(ctx.local_db, instance_id, &message, 30).await?; + tracing::info!( + "Defer PeginConfirmPartialSig for {instance_id}: waiting for committee pub nonces" + ); + return Ok(()); + } + let received_pub_nonce = match pub_nonces_unchecked + .iter() + .find(|(pubkey, _)| pubkey == received_committee_pubkey) + .map(|(_, pub_nonce)| pub_nonce.clone()) + { + Some(pub_nonce) => pub_nonce, + None => { + tracing::warn!( + "Ignore PeginConfirmPartialSig for {instance_id} from {}: missing signer pub nonce", + received_committee_pubkey + ); + return Ok(()); + } + }; + let pub_nonces = match order_committee_values( + &committee_pubkeys, + pub_nonces_unchecked, + "pegin committee pub nonces", + ) { + Ok(pub_nonces) => pub_nonces, + Err(e) => { + tracing::warn!( + "Ignore PeginConfirmPartialSig for {instance_id} from {}: invalid pub nonce set: {e}", + received_committee_pubkey + ); + return Ok(()); + } + }; + let agg_nonce = nonce_aggregation(&pub_nonces); + let instance_params = get_instance_parameters(ctx.local_db, instance_id) + .await? + .ok_or_else(|| anyhow!("Instance parameters not found for {instance_id}"))?; + if let Err(e) = verify_pegin_confirm_partial_sig( + &instance_params, + &committee_pubkeys, + received_committee_pubkey, + &received_pub_nonce, + &agg_nonce, + *partial_sig, + ) { + tracing::warn!( + "Ignore PeginConfirmPartialSig for {instance_id} from {}: invalid partial signature: {e}", + received_committee_pubkey + ); + return Ok(()); + } + let pegin_confirm = instance_params.build_pegin_tx()?.1; + let pegin_txid = pegin_confirm.tx().compute_txid(); + match verify_pegin_endorsement( + ctx.goat_client, + instance_id, + received_committee_pubkey, + &pegin_txid, + endorse_sig, + ) + .await + { + Ok(true) => {} + Ok(false) => { + tracing::warn!( + "Ignore PeginConfirmPartialSig for {instance_id} from {}: invalid endorsement signature", + received_committee_pubkey + ); + return Ok(()); + } + Err(e) => { + tracing::warn!( + "Ignore PeginConfirmPartialSig for {instance_id} from {}: failed to verify endorsement signature: {e}", + received_committee_pubkey + ); + return Ok(()); + } + } + // 1. save the validated partial signature & endorsement signature to local db store_committee_partial_sig_for_instance( ctx.local_db, instance_id, @@ -2527,33 +2809,31 @@ async fn handle_pegin_confirm_partial_sig_committee( .await?; // 3. (Relayer) if received enough partial signatures, aggregate the sigs if is_relayer() { - let committee_pubkeys = ctx.goat_client.gateway_get_committee_pubkeys(&instance_id).await?; let pub_nonces = get_committee_pub_nonces_for_instance(ctx.local_db, instance_id).await?; - let partial_sigs = get_committee_partial_sigs_for_instance(ctx.local_db, instance_id) - .await? - .into_iter() - .map(|(_, ps)| ps) - .collect::>(); + let partial_sigs = + get_committee_partial_sigs_for_instance(ctx.local_db, instance_id).await?; if pub_nonces.len() == committee_pubkeys.len() && partial_sigs.len() == committee_pubkeys.len() { - let instance_params = get_instance_parameters(ctx.local_db, instance_id) - .await? - .ok_or_else(|| anyhow!("Instance parameters not found for {instance_id}"))?; let mut pegin_confirm = instance_params.build_pegin_tx()?.1; - let agg_nonce = - nonce_aggregation(&pub_nonces.iter().map(|(_, pn)| pn.clone()).collect::>()); + let pub_nonces = order_committee_values( + &committee_pubkeys, + pub_nonces, + "pegin committee pub nonces", + )?; + let partial_sigs = order_committee_values( + &committee_pubkeys, + partial_sigs, + "pegin committee partial sigs", + )?; + let agg_nonce = nonce_aggregation(&pub_nonces); let context = instance_params.get_base_context(); let full_sig = pegin_confirm .aggregate_input_0_musig2_signatures(&context, partial_sigs, &agg_nonce) .map_err(|e| { anyhow!("Failed to aggregate Pegin-Confirm's signatures for {instance_id}: {e}") })?; - let connector_z = ConnectorZ::new( - context.network, - &context.n_of_n_taproot_public_key, - &instance_params.user_info.user_xonly_pubkey, - ); + let connector_z = instance_params.connector_z(); pegin_confirm.push_input_0_signature(&connector_z, full_sig); broadcast_tx(ctx.btc_client, pegin_confirm.tx()).await?; } @@ -2786,7 +3066,10 @@ async fn handle_kickoff_ready_operator( ); let nonce_interval = graph.parameters.graph_nonce - current_graph.parameters.graph_nonce; - let min_pegout_time_secs = take1_timelock(ctx.btc_client.network()) as u64 + let min_pegout_time_secs = take1_timelock_with_config( + ctx.btc_client.network(), + ¤t_graph.parameters.timelock_config, + ) as u64 * todo_funcs::avg_block_time_secs(ctx.btc_client.network()); let delay_secs = min_pegout_time_secs * nonce_interval; push_local_unhandled_messages( @@ -2820,7 +3103,10 @@ async fn handle_kickoff_ready_operator( ); let nonce_interval = graph.parameters.graph_nonce - current_graph.parameters.graph_nonce; - let min_pegout_time_secs = take1_timelock(ctx.btc_client.network()) as u64 + let min_pegout_time_secs = take1_timelock_with_config( + ctx.btc_client.network(), + ¤t_graph.parameters.timelock_config, + ) as u64 * todo_funcs::avg_block_time_secs(ctx.btc_client.network()); let delay_secs = min_pegout_time_secs * nonce_interval; push_local_unhandled_messages( @@ -2980,13 +3266,7 @@ async fn handle_kickoff_sent_verifier( let (challenge_tx, _) = export_challenge_tx(&graph).unwrap(); let challenge_txid = challenge_tx.compute_txid(); if ctx.btc_client.get_tx(&challenge_txid).await?.is_none() { - let challenge_txid = send_challenge_tx(ctx.btc_client, &graph).await?; - let message_content = GOATMessageContent::ChallengeSent(ChallengeSent { - instance_id, - graph_id, - challenge_txid, - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; + send_challenge_tx(ctx.btc_client, &graph).await?; } } } else { @@ -3174,12 +3454,6 @@ async fn handle_challenge_sent_operator( } None => broadcast_tx(ctx.btc_client, &watchtower_challenge_init_tx).await?, }; - let message_content = - GOATMessageContent::WatchtowerChallengeInitSent(WatchtowerChallengeInitSent { - instance_id, - graph_id, - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::Watchtower, message_content)).await?; Ok(()) } @@ -3297,12 +3571,6 @@ async fn handle_watchtower_challenge_init_sent_watchtower( tracing::info!( "WatchtowerChallengeSent for {instance_id}:{graph_id}: watchtower_index={node_index}, txid={watchtower_challenge_txid}" ); - let message_content = GOATMessageContent::WatchtowerChallengeSent(WatchtowerChallengeSent { - instance_id, - graph_id, - watchtower_index: node_index, - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::Operator, message_content)).await?; Ok(()) } @@ -3571,16 +3839,43 @@ async fn handle_operator_commit_pubin_ready_operator( let watchtower_challenge_init_txid = SerializableTxid::from(graph.watchtower_challenge_init.tx().compute_txid()); let num_watchtowers = graph.parameters.watchtower_pubkeys.len(); - let (challenge_txids, included_watchtowers_bits) = - get_watchtower_challenge_info(ctx.btc_client, &watchtower_challenge_init_txid, num_watchtowers) - .await?; + let watchtower_timeout_txids: Vec = + graph.watchtower_challenge_timeouts.iter().map(|tx| tx.tx().compute_txid()).collect(); + let wait_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()) as usize; + let (challenge_txids, included_watchtowers_bits) = match get_watchtower_challenge_info( + ctx.btc_client, + &watchtower_challenge_init_txid, + &watchtower_timeout_txids, + num_watchtowers, + ) + .await + { + Ok(info) => info, + Err(e) => { + tracing::info!( + "Retry OperatorCommitPubinReady later for {instance_id}:{graph_id}: challenge info is not ready: {e}" + ); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, wait_secs).await?; + return Ok(()); + } + }; let (btc_best_block_hash, included_watchtowers_bitmap) = - compute_operator_pubin_blockhash_and_bitmap( + match compute_operator_pubin_blockhash_and_bitmap( ctx.btc_client, &challenge_txids, &included_watchtowers_bits, ) - .await?; + .await + { + Ok(inputs) => inputs, + Err(e) => { + tracing::info!( + "Retry OperatorCommitPubinReady later for {instance_id}:{graph_id}: operator pubin inputs are not ready: {e}" + ); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, wait_secs).await?; + return Ok(()); + } + }; let guest_pubin = build_operator_guest_pubin( &btc_best_block_hash, &graph.parameters.pubin_disprove_constant, @@ -3604,7 +3899,7 @@ async fn handle_operator_commit_pubin_ready_operator( connector_e_amount, vec![], ) - .await?; + .await?; Ok(()) } @@ -3669,7 +3964,6 @@ async fn handle_operator_commit_pubin_timeout_verifier( Ok(()) } - // after the watchtower challenge flow is complete, build proof and broadcast Assert transaction. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_assert_ready_operator( @@ -3717,8 +4011,14 @@ async fn handle_assert_ready_operator( operator_proof.public_inputs.len() ); } - let dynamic_input = operator_proof.public_inputs[1]; - let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; + let dynamic_input = operator_proof.public_inputs.get(1).copied().ok_or_else(|| { + anyhow!( + "operator proof has {} public inputs; expected dynamic input at index 1", + operator_proof.public_inputs.len() + ) + })?; + let assert_witness = + build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; let mut asserted_operator_proof = Vec::new(); operator_proof.proof.serialize_compressed(&mut asserted_operator_proof)?; @@ -3739,14 +4039,6 @@ async fn handle_assert_ready_operator( let assert_tx = operator_sign_assert(&mut graph, &assert_secret_key, &assert_message)?; broadcast_nonstandard_tx(ctx.btc_client, &assert_tx).await?; - let message_content = GOATMessageContent::AssertSent(AssertSent { - instance_id, - graph_id, - assert_txid: assert_tx.compute_txid(), - assert_witness: Some(assert_witness), - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::Verifier, message_content)).await?; - Ok(()) } @@ -3785,22 +4077,28 @@ fn recover_challenge_assert_witness( } /// Collects the on-chain ACK TxIns from all watchtowers that submitted a challenge_ack. -/// Only connectors that are already spent are included; unspent ones are skipped. +/// Unspent ACK connectors are skipped; spent connectors must resolve to the spending input. async fn collect_ack_txins( btc_client: &BTCClient, wci_txid: &Txid, - num_watchtowers: usize, + watchtower_timeout_txids: &[Txid], ) -> Result> { let mut ack_txins = Vec::new(); - for i in 0..num_watchtowers { + for (i, timeout_txid) in watchtower_timeout_txids.iter().enumerate() { let ack_vout = output_topology::watchtower_challenge_init::ack_connector(i) as u64; - if let Some(ack_txid) = outpoint_spent_txid(btc_client, wci_txid, ack_vout).await? { - if let Some(ack_tx) = btc_client.get_tx(&ack_txid).await? { - if let Some(txin) = ack_tx.input.first().cloned() { - ack_txins.push(txin); - } - } + let Some(ack_spent_txid) = outpoint_spent_txid(btc_client, wci_txid, ack_vout).await? + else { + continue; + }; + if &ack_spent_txid == timeout_txid { + continue; } + let Some((_spent_txid, _vin, txin)) = + outpoint_spent_txin(btc_client, wci_txid, ack_vout).await? + else { + bail!("ACK spending tx {ack_spent_txid} for watchtower {i} is unavailable"); + }; + ack_txins.push(txin); } Ok(ack_txins) } @@ -3812,7 +4110,7 @@ async fn handle_assert_sent_verifier( instance_id: Uuid, graph_id: Uuid, assert_txid: Txid, - assert_witness: &Option, + content: &GOATMessageContent, ) -> Result<()> { let (graph, _graph_status, _graph_sub_status) = match refresh_graph_status(ctx, instance_id, graph_id, None, GraphStatus::Challenge).await? @@ -3853,9 +4151,42 @@ async fn handle_assert_sent_verifier( .first() .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; let wci_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let num_watchtowers = graph.parameters.watchtower_ack_hashlocks.len(); - let ack_txins = collect_ack_txins(ctx.btc_client, &wci_txid, num_watchtowers).await?; - match validate_pubin_disprove(&graph, commit_pubin_txin, assert_txin, &ack_txins) { + let watchtower_timeout_txids = graph + .watchtower_challenge_timeouts + .iter() + .map(|tx| tx.tx().compute_txid()) + .collect::>(); + let ack_txins = match collect_ack_txins( + ctx.btc_client, + &wci_txid, + &watchtower_timeout_txids, + ) + .await + { + Ok(txins) => txins, + Err(e) => { + let delay_secs = + todo_funcs::avg_block_time_secs(ctx.btc_client.network()); + let message = make_message(ctx, content); + push_local_unhandled_messages( + ctx.local_db, + graph_id, + &message, + delay_secs as usize, + ) + .await?; + tracing::info!( + "Retry AssertSent later for {instance_id}:{graph_id}: ACK inputs are not ready: {e}" + ); + return Ok(()); + } + }; + match validate_pubin_disprove( + &graph, + commit_pubin_txin, + assert_txin, + &ack_txins, + ) { Ok(Some((witness_data, _))) => { // build pubin disprove Tx let pubin_disprove_txin = @@ -3867,28 +4198,27 @@ async fn handle_assert_sent_verifier( output: vec![goat::scripts::p2a_output()], }; broadcast_nonstandard_tx(ctx.btc_client, &pubin_disprove_tx).await?; - let message_content = - GOATMessageContent::DisproveSent(DisproveSent { - instance_id, - graph_id, - disprove_type: DisproveTxType::PubinDisprove, - index: verifier_index, - challenge_start_txid: None, - challenge_finish_txid: pubin_disprove_tx.compute_txid(), - }); - send_to_peer( - ctx.swarm, - GOATMessage::new(Actor::Committee, message_content), - ) - .await?; return Ok(()); } Ok(None) => tracing::debug!( "PubinDisprove invalid for {instance_id}:{graph_id}: operator pubin consistent, proceeding to ChallengeAssert" ), - Err(e) => tracing::warn!( - "PubinDisprove check failed for {instance_id}:{graph_id}: {e}, proceeding to ChallengeAssert" - ), + Err(e) => { + let delay_secs = + todo_funcs::avg_block_time_secs(ctx.btc_client.network()); + let message = make_message(ctx, content); + push_local_unhandled_messages( + ctx.local_db, + graph_id, + &message, + delay_secs as usize, + ) + .await?; + tracing::warn!( + "Retry AssertSent later for {instance_id}:{graph_id}: PubinDisprove check failed: {e}" + ); + return Ok(()); + } } } } @@ -3896,29 +4226,21 @@ async fn handle_assert_sent_verifier( } // recover TxAssertWitness from the assert tx witness and graph. - let assert_witness_recovered: TxAssertWitness; - let assert_witness: &TxAssertWitness = match assert_witness { - Some(w) => w, - None => { - let raw_witness = graph - .connector_c() - .extract_leaf_1_raw_witness( - assert_tx - .input - .first() - .ok_or_else(|| anyhow!("operator assert transaction has no input"))?, - ) - .map_err(|e| anyhow!("failed to extract WOTS signature from assert tx: {e}"))?; - let mut bitcoin_witness = bitcoin::Witness::new(); - for item in &raw_witness { - bitcoin_witness.push(item); - } - assert_witness_recovered = TxAssertWitness { - wots_sig: Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec(), - }; - &assert_witness_recovered - } - }; + let raw_witness = graph + .connector_c() + .extract_leaf_1_raw_witness( + assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?, + ) + .map_err(|e| anyhow!("failed to extract WOTS signature from assert tx: {e}"))?; + let mut bitcoin_witness = bitcoin::Witness::new(); + for item in &raw_witness { + bitcoin_witness.push(item); + } + let assert_witness = + TxAssertWitness { wots_sig: Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec() }; let Some(saved_verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .and_then(|state| state.verifier) @@ -3943,7 +4265,7 @@ async fn handle_assert_sent_verifier( &vk, static_input, &graph.parameters.operator_assert_wots_pubkey, - assert_witness, + &assert_witness, verifier_index, )?; let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness @@ -3968,14 +4290,6 @@ async fn handle_assert_sent_verifier( let challenge_assert_tx = build_verifier_assert_tx(&graph, operator_assert_txin, verifier_index, labels)?; broadcast_nonstandard_tx(ctx.btc_client, &challenge_assert_tx).await?; - let message_content = GOATMessageContent::ChallengeAssertSent(ChallengeAssertSent { - instance_id, - graph_id, - challenge_assert_txid: challenge_assert_tx.compute_txid(), - verifier_index, - challenge_witness: Some(challenge_witness), - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::Operator, message_content)).await?; Ok(()) } @@ -3988,7 +4302,6 @@ async fn handle_challenge_assert_sent_operator( graph_id: Uuid, challenge_assert_txid: Txid, verifier_index: usize, - challenge_witness: &Option, content: &GOATMessageContent, ) -> Result<()> { let (graph, _graph_status, _graph_sub_status) = @@ -4011,16 +4324,8 @@ async fn handle_challenge_assert_sent_operator( return Ok(()); }; - let challenge_witness_recovered: BabeChallengeAssertWitness; - let challenge_witness: &BabeChallengeAssertWitness = match challenge_witness { - Some(w) => w, - None => { - challenge_witness_recovered = - recover_challenge_assert_witness(&challenge_assert_tx, verifier_index)?; - &challenge_witness_recovered - } - }; - validate_challenge_witness_index(verifier_index, challenge_witness)?; + let challenge_witness = recover_challenge_assert_witness(&challenge_assert_tx, verifier_index)?; + validate_challenge_witness_index(verifier_index, &challenge_witness)?; let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness .witness @@ -4079,12 +4384,21 @@ async fn handle_challenge_assert_sent_operator( .ok_or_else(|| anyhow!("missing asserted operator proof for graph {graph_id}"))?; let proof = Groth16Proof::deserialize_compressed(proof_bytes.as_slice()) .context("deserialize asserted operator proof")?; - let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE wrongly challenged")?; + let vk = crate::vk::get_vk() + .await + .context("load Groth16 verifying key for BABE wrongly challenged")?; let (_, dyn_pubin) = TxAssertWitness { wots_sig: challenge_witness.witness.wots_sig.clone() } .recover_pi1_xd_without_verify() - .ok_or_else(|| anyhow!("cannot recover dynamic input from challenge witness WOTS signature"))?; - let wrongly_challenged_witness = - recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof, vk, dyn_pubin)?; + .ok_or_else(|| { + anyhow!("cannot recover dynamic input from challenge witness WOTS signature") + })?; + let wrongly_challenged_witness = recover_real_wrongly_challenged_witness( + prover_state, + &challenge_witness, + &proof, + vk, + dyn_pubin, + )?; let (wrongly_challenged_input, _amount) = operator_sign_wrongly_challenged( &graph, verifier_index, @@ -4164,7 +4478,10 @@ async fn handle_wrongly_challenge_timeout_verifier( }; let disprove_height = challenge_assert_height - + disprove_timelock(graph.parameters.instance_parameters.network) as u64; + + disprove_timelock_with_config( + graph.parameters.instance_parameters.network, + &graph.parameters.timelock_config, + ) as u64; let goat_confirmed_height = ctx.goat_client.btc_spv_latest_height().await?; if goat_confirmed_height < disprove_height { let retry_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()) @@ -4187,16 +4504,6 @@ async fn handle_wrongly_challenge_timeout_verifier( let disprove_tx = build_disprove_tx(&graph, verifier_index, None)?; broadcast_nonstandard_tx(ctx.btc_client, &disprove_tx).await?; - let message_content = GOATMessageContent::DisproveSent(DisproveSent { - instance_id, - graph_id, - disprove_type: DisproveTxType::Disprove, - index: verifier_index, - challenge_start_txid: Some(challenge_assert_txid), - challenge_finish_txid: disprove_tx.compute_txid(), - }); - send_to_peer(ctx.swarm, GOATMessage::new(Actor::Committee, message_content)).await?; - Ok(()) } @@ -4362,7 +4669,9 @@ async fn handle_take1_ready_operator( return Ok(()); } }; - if !is_take1_timelock_expired(ctx.btc_client, kickoff_height).await? { + if !is_take1_timelock_expired(ctx.btc_client, kickoff_height, &graph.parameters.timelock_config) + .await? + { tracing::warn!( "Ignore Take1Ready for {instance_id}:{graph_id}: kickoff tx timelock not expired yet" ); @@ -4502,7 +4811,8 @@ async fn handle_take2_ready_operator( return Ok(()); } let operator_assert_txid = graph.operator_assert.tx().compute_txid(); - // Take2 spends Connector-0, Connector-D, and Guardian Connector. Recheck every input before + let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); + // Take2 spends Connector-0, Connector-D, Connector-F, and Guardian Connector. Recheck every input before // signing so a stale Take2Ready cannot race an already-spent connector. let take2_inputs = graph.take2.tx().input.iter().map(|txin| txin.previous_output).collect::>(); @@ -4534,7 +4844,28 @@ async fn handle_take2_ready_operator( return Ok(()); } }; - if !is_take2_timelock_expired(ctx.btc_client, operator_assert_height).await? { + let watchtower_challenge_init_height = match ctx + .btc_client + .get_tx_status(&watchtower_challenge_init_txid) + .await? + .block_height + { + Some(height) => height, + None => { + tracing::warn!( + "Ignore Take2Ready for {instance_id}:{graph_id}: watchtower challenge init tx not confirmed yet" + ); + return Ok(()); + } + }; + if !is_take2_timelock_expired( + ctx.btc_client, + operator_assert_height, + watchtower_challenge_init_height, + &graph.parameters.timelock_config, + ) + .await? + { tracing::warn!( "Ignore Take2Ready for {instance_id}:{graph_id}: take2 timelock not expired yet" ); @@ -4723,11 +5054,11 @@ async fn handle_response_node_info( #[cfg(test)] mod tests { - use std::str::FromStr; use bitcoin::hashes::Hash; use bitvm_lib::babe_adapter::{ BABE_M_CC, BabeProverState, build_setup_package, open_and_solder, }; + use std::str::FromStr; use super::*; @@ -4741,7 +5072,8 @@ mod tests { let selected = (0..BABE_M_CC).collect::>(); let (_, finalized, soldering) = open_and_solder(&package, &selected).unwrap(); 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 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).unwrap(); let prover_state = BabeProverState { package: package.clone(), @@ -4886,20 +5218,23 @@ mod tests { let (btc_client, mock) = client::btc_chain::BTCClient::new_mock_client(); let wci_txid = make_txid(0x00); let ack_txid_0 = make_txid(0x01); - let num_watchtowers = 3; + let timeout_txid_1 = make_txid(0x03); + let timeout_txids = vec![make_txid(0x10), timeout_txid_1, make_txid(0x12)]; // Watchtower 0 ACKed let vout_0 = output_topology::watchtower_challenge_init::ack_connector(0) as u32; mock.set_tx(ack_txid_0, make_confirmed_ack_tx(ack_txid_0, wci_txid, vout_0)); - // Watchtower 1 did NOT ACK (connector unspent — nothing registered) + // Watchtower 1 timed out; the timeout spend must not count as an ACK. + let vout_1 = output_topology::watchtower_challenge_init::ack_connector(1) as u32; + mock.set_tx(timeout_txid_1, make_confirmed_ack_tx(timeout_txid_1, wci_txid, vout_1)); // Watchtower 2 ACKed let ack_txid_2 = make_txid(0x02); let vout_2 = output_topology::watchtower_challenge_init::ack_connector(2) as u32; mock.set_tx(ack_txid_2, make_confirmed_ack_tx(ack_txid_2, wci_txid, vout_2)); - let txins = collect_ack_txins(&btc_client, &wci_txid, num_watchtowers).await.unwrap(); + let txins = collect_ack_txins(&btc_client, &wci_txid, &timeout_txids).await.unwrap(); assert_eq!(txins.len(), 2); assert_eq!(txins[0].previous_output.vout, vout_0); @@ -4938,9 +5273,8 @@ mod tests { let msg = [0x42u8; 96]; let sig = Wots96::sign(&sk, &msg); - let labels: Vec<[u8; 16]> = (0..goat::assert_scripts::INPUT_WIRE_NUM) - .map(|i| [(i % 256) as u8; 16]) - .collect(); + let labels: Vec<[u8; 16]> = + (0..goat::assert_scripts::INPUT_WIRE_NUM).map(|i| [(i % 256) as u8; 16]).collect(); let tx = build_challenge_assert_tx(&sig, &labels); let verifier_index = 3; diff --git a/node/src/scheduled_tasks/graph_maintenance_tasks.rs b/node/src/scheduled_tasks/graph_maintenance_tasks.rs index dfed5b02..6489e2d4 100644 --- a/node/src/scheduled_tasks/graph_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/graph_maintenance_tasks.rs @@ -7,18 +7,17 @@ use crate::action::{ use crate::env::get_network; use crate::rpc_service::current_time_secs; use crate::scheduled_tasks::fetch_on_turn_graph_by_status; -use crate::utils::{SELF_SENDER, outpoint_spent_txid, upsert_message}; +use crate::utils::{SELF_SENDER, outpoint_spent_txid, parse_graph_raw_data, upsert_message}; use bitcoin::Txid; use bitvm_lib::actors::Actor; -use bitvm_lib::operator::{take1_timelock, take2_timelock}; -use bitvm_lib::verifier::disprove_timelock; +use bitvm_lib::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, validate_timelock_config, watchtower_challenge_timelock_blocks, +}; use client::btc_chain::BTCClient; use client::goat_chain::DisproveTxType; -use goat::{ - constants::{OPERATOR_ACK_TIMELOCK, OPERATOR_COMMIT_TIMELOCK, WATCHTOWER_CHALLENGE_TIMELOCK}, - transactions::base::output_topology, - utils::num_blocks_per_network, -}; +use goat::{constants::TimelockConfig, transactions::base::output_topology}; use serde::{Deserialize, Serialize}; use store::localdb::{LocalDB, StorageProcessor}; use store::{ @@ -33,18 +32,6 @@ const MONITE_BTC_TX_NAME_WATCHTOWER_INIT: &str = "watchtower_init"; const MONITE_BTC_TX_NAME_PROVER_ASSERT: &str = "prover_assert"; const MONITE_BTC_TX_NAME_VERIFIER_ASSERT: &str = "verifier_assert"; -fn get_take1_timelock_config() -> i64 { - take1_timelock(get_network()) as i64 -} - -fn get_take2_timelock_config() -> i64 { - take2_timelock(get_network()) as i64 -} - -fn get_disprove_timelock_config() -> i64 { - disprove_timelock(get_network()) as i64 -} - #[derive(Clone, Debug, Eq, PartialEq, Display, EnumString)] enum OperatorWithdrawType { Take1, @@ -86,6 +73,27 @@ impl DetectedGraphMessage { } } +async fn graph_timelock_config( + local_db: &LocalDB, + graph_id: Uuid, +) -> anyhow::Result { + let raw_data = { + let mut storage_processor = local_db.acquire().await?; + storage_processor.find_graph_raw_data(&graph_id).await? + }; + let Some(raw_data) = raw_data else { + warn!("graph {graph_id} raw data is missing, fallback to default timelock config"); + return Ok(default_timelock_config(get_network())); + }; + + let graph = parse_graph_raw_data(raw_data.raw_data, graph_id).await?; + validate_timelock_config( + graph.parameters.instance_parameters.network, + &graph.parameters.timelock_config, + )?; + Ok(graph.parameters.timelock_config) +} + impl ChallengeSubStatus { pub fn is_watchtower_challenge_success(&self, required_watchtower_num: usize) -> bool { self.watchtower_challenge_status @@ -171,6 +179,46 @@ async fn get_confirmed_tx_monitor( Ok(Some(monitor)) } +async fn detect_watchtower_flow_disprove( + btc_client: &BTCClient, + graph: &Graph, +) -> anyhow::Result> { + for (index, txid) in graph.operator_challenge_nack_txids.iter().enumerate() { + if btc_client.get_tx_status(&txid.0).await?.confirmed { + return Ok(Some(DetectedGraphMessage::with_sub_type( + Actor::Committee, + GOATMessageContent::DisproveSent(DisproveSent { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + disprove_type: DisproveTxType::OperatorChallengeNack, + index, + challenge_start_txid: None, + challenge_finish_txid: txid.0, + }), + index.to_string(), + ))); + } + } + + if let Some(txid) = graph.operator_commit_timeout_txid.clone() + && btc_client.get_tx_status(&txid.0).await?.confirmed + { + return Ok(Some(DetectedGraphMessage::new( + Actor::Committee, + GOATMessageContent::DisproveSent(DisproveSent { + instance_id: graph.instance_id, + graph_id: graph.graph_id, + disprove_type: DisproveTxType::OperatorCommitTimeout, + index: 0, + challenge_start_txid: None, + challenge_finish_txid: txid.0, + }), + ))); + } + + Ok(None) +} + fn is_timelock_ready(confirmed_height: i64, lock_blocks: i64, current_height: i64) -> bool { confirmed_height > 0 && confirmed_height + lock_blocks <= current_height } @@ -302,8 +350,9 @@ pub async fn detect_take1_or_challenge( "start tick action: detect_take1_or_challenge, graphs: {}, current_height: {current_height}", graphs.len() ); - let lock_blocks = get_take1_timelock_config(); for graph in graphs { + let timelock_config = graph_timelock_config(local_db, graph.graph_id).await?; + let lock_blocks = take1_timelock_blocks(get_network(), &timelock_config) as i64; if detect_kickoff_ref_disprove_tx(btc_client, local_db, &graph).await? { warn!( "process_graph_challenge detect_kickoff_ref_disprove_tx happened at graph:{}", @@ -454,6 +503,7 @@ async fn detect_watchtower_flow( ); return Ok(vec![]); }; + let timelock_config = graph_timelock_config(local_db, graph.graph_id).await?; let mut messages = vec![DetectedGraphMessage::new( Actor::Watchtower, @@ -470,6 +520,10 @@ async fn detect_watchtower_flow( return Ok(messages); } + if let Some(disprove_message) = detect_watchtower_flow_disprove(btc_client, graph).await? { + return Ok(vec![disprove_message]); + } + let connector_e_vout = output_topology::watchtower_challenge_init::connector_e(watchtower_num) as u64; let connector_f_vout = @@ -488,7 +542,7 @@ async fn detect_watchtower_flow( let pubin_commit_completed = connector_e_spent_txid.is_some() && !operator_commit_timeout_on_chain; - let mut all_watchtower_connectors_spent = true; + let mut all_watchtower_branches_resolved = true; let mut any_watchtower_timeout_ready = false; let mut any_nack_ready = false; for watchtower_index in 0..watchtower_num { @@ -504,13 +558,26 @@ async fn detect_watchtower_flow( outpoint_spent_txid(btc_client, &watchtower_challenge_init_txid, ack_vout).await?; let timeout_txid = graph.watchtower_challenge_timeout_txids.get(watchtower_index).cloned().map(Into::into); + let watchtower_spend_confirmed = match watchtower_spent_txid.as_ref() { + Some(txid) => btc_client.get_tx_status(txid).await?.confirmed, + None => false, + }; + let ack_spend_confirmed = match ack_spent_txid.as_ref() { + Some(txid) => btc_client.get_tx_status(txid).await?.confirmed, + None => false, + }; let watchtower_timeout_spent = match (watchtower_spent_txid.as_ref(), timeout_txid.as_ref()) { - (Some(spent_txid), Some(timeout_txid)) => spent_txid == timeout_txid, + (Some(spent_txid), Some(timeout_txid)) => { + spent_txid == timeout_txid && watchtower_spend_confirmed + } _ => false, }; match watchtower_spent_txid { + Some(_) if !watchtower_spend_confirmed => { + all_watchtower_branches_resolved = false; + } Some(_) if !watchtower_timeout_spent => { messages.push(DetectedGraphMessage::with_sub_type( Actor::Operator, @@ -522,24 +589,28 @@ async fn detect_watchtower_flow( watchtower_index.to_string(), )); - if ack_spent_txid.is_none() - && connector_f_spent_txid.is_none() - && is_timelock_ready( - monitor.height, - num_blocks_per_network(get_network(), OPERATOR_ACK_TIMELOCK) as i64, - current_height, - ) - { - any_nack_ready = true; + if !ack_spend_confirmed { + all_watchtower_branches_resolved = false; + if connector_f_spent_txid.is_none() + && ack_spent_txid.is_none() + && is_timelock_ready( + monitor.height, + operator_ack_timelock_blocks(get_network(), &timelock_config) as i64, + current_height, + ) + { + any_nack_ready = true; + } } } Some(_) => {} None => { - all_watchtower_connectors_spent = false; + all_watchtower_branches_resolved = false; if ack_spent_txid.is_none() && is_timelock_ready( monitor.height, - num_blocks_per_network(get_network(), WATCHTOWER_CHALLENGE_TIMELOCK) as i64, + watchtower_challenge_timelock_blocks(get_network(), &timelock_config) + as i64, current_height, ) { @@ -569,7 +640,7 @@ async fn detect_watchtower_flow( )); } - if all_watchtower_connectors_spent + if all_watchtower_branches_resolved && connector_e_spent_txid.is_none() && connector_f_spent_txid.is_none() { @@ -583,7 +654,7 @@ async fn detect_watchtower_flow( if is_timelock_ready( monitor.height, - num_blocks_per_network(get_network(), OPERATOR_COMMIT_TIMELOCK) as i64, + operator_commit_timelock_blocks(get_network(), &timelock_config) as i64, current_height, ) { messages.push(DetectedGraphMessage::new( @@ -596,7 +667,7 @@ async fn detect_watchtower_flow( } } - if all_watchtower_connectors_spent + if all_watchtower_branches_resolved && pubin_commit_completed && let Some(operator_assert_txid) = graph.operator_assert_txid.clone() && !btc_client.get_tx_status(&operator_assert_txid.0).await?.confirmed @@ -654,7 +725,6 @@ async fn detect_assert_sent_flow( instance_id: graph.instance_id, graph_id: graph.graph_id, assert_txid: operator_assert_txid, - assert_witness: None, }), )]; @@ -679,7 +749,6 @@ async fn detect_assert_sent_flow( graph_id: graph.graph_id, challenge_assert_txid, verifier_index, - challenge_witness: None, }), verifier_index.to_string(), )); @@ -719,6 +788,9 @@ async fn detect_assert_disprove_ready( return Ok(None); } + let timelock_config = graph_timelock_config(local_db, graph.graph_id).await?; + let disprove_timelock = disprove_timelock_blocks(get_network(), &timelock_config) as i64; + for (index, verifier_assert_txid) in graph.verifier_assert_txids.iter().enumerate() { let verifier_assert_txid: Txid = verifier_assert_txid.clone().into(); if outpoint_spent_txid(btc_client, &verifier_assert_txid, 0).await?.is_some() { @@ -761,7 +833,7 @@ async fn detect_assert_disprove_ready( height }; - if height + get_disprove_timelock_config() <= current_height { + if height + disprove_timelock <= current_height { info!( "detect_assert_disprove_ready graph_id:{} verifier_assert index:{} is ready to disprove", graph.graph_id, index @@ -773,7 +845,6 @@ async fn detect_assert_disprove_ready( graph_id: graph.graph_id, challenge_assert_txid: verifier_assert_txid, verifier_index: index, - wrongly_challenged_witness: None, }), Some(index.to_string()), ))); @@ -1022,17 +1093,26 @@ async fn detect_take2( graph: &Graph, current_height: i64, ) -> anyhow::Result> { - let (kickoff_txid, operator_assert_txid, take2_txid) = match ( + let (kickoff_txid, watchtower_challenge_init_txid, operator_assert_txid, take2_txid) = match ( graph.kickoff_txid.clone(), + graph.watchtower_challenge_init_txid.clone(), graph.operator_assert_txid.clone(), graph.take2_txid.clone(), ) { - (Some(kickoff_txid), Some(operator_assert_txid), Some(take2_txid)) => { - (kickoff_txid.into(), operator_assert_txid.into(), take2_txid.into()) - } + ( + Some(kickoff_txid), + Some(watchtower_challenge_init_txid), + Some(operator_assert_txid), + Some(take2_txid), + ) => ( + kickoff_txid.into(), + watchtower_challenge_init_txid.into(), + operator_assert_txid.into(), + take2_txid.into(), + ), _ => { warn!( - "detect_take2 graph_id:{} kickoff_txid/operator_assert_txid/take2_txid has none value", + "detect_take2 graph_id:{} kickoff_txid/watchtower_challenge_init_txid/operator_assert_txid/take2_txid has none value", graph.graph_id ); return Ok(None); @@ -1113,17 +1193,27 @@ async fn detect_take2( .unwrap_or_default() .height }; + let timelock_config = graph_timelock_config(local_db, graph.graph_id).await?; if check_operator_withdraw_ready_condition( btc_client, local_db, graph.graph_id, - vec![( - operator_assert_txid, - MONITE_BTC_TX_NAME_PROVER_ASSERT.to_string(), - OperatorWithdrawType::Take2, - height, - get_take2_timelock_config(), - )], + vec![ + ( + operator_assert_txid, + MONITE_BTC_TX_NAME_PROVER_ASSERT.to_string(), + OperatorWithdrawType::Take2, + height, + take2_timelock_blocks(get_network(), &timelock_config) as i64, + ), + ( + watchtower_challenge_init_txid, + MONITE_BTC_TX_NAME_WATCHTOWER_INIT.to_string(), + OperatorWithdrawType::Take2, + 0, + connector_f_timelock_blocks(get_network(), &timelock_config) as i64, + ), + ], current_height, ) .await? diff --git a/node/src/scheduled_tasks/instance_maintenance_tasks.rs b/node/src/scheduled_tasks/instance_maintenance_tasks.rs index c7dd23b7..6d5d886c 100644 --- a/node/src/scheduled_tasks/instance_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/instance_maintenance_tasks.rs @@ -1,7 +1,7 @@ use crate::action::{ConfirmInstance, GOATMessageContent, PeginRequest, PostReady}; use crate::env::{ - COMMITTEE_INSTANCE_KEYS_DIR, INSTANCE_PRESIGNED_TIME_EXPIRED, get_bitvm_key, - get_committee_instance_key_delete_timelock_blocks, get_instance_maintenance_batch_size, + COMMITTEE_INSTANCE_KEYS_DIR, get_bitvm_key, get_committee_instance_key_delete_timelock_blocks, + get_instance_maintenance_batch_size, get_instance_presigned_time_expired_secs, is_enable_committee_instance_key_delete, }; use crate::rpc_service::current_time_secs; @@ -14,8 +14,8 @@ use crate::utils::{ }; use alloy::sol_types::SolType; use bitvm_lib::actors::Actor; -use bitvm_lib::constants::CONNECTOR_Z_TIMELOCK; use bitvm_lib::keys::CommitteeMasterKey; +use bitvm_lib::timelocks::default_connector_z_timelock_blocks; use bitvm_lib::transactions::base::BaseTransaction; use client::Utxo; use client::btc_chain::BTCClient; @@ -293,7 +293,7 @@ pub async fn instance_expiration_monitor( .update_expired_instance( &InstanceBridgeInStatus::UserBroadcastPeginPrepare.to_string(), &InstanceBridgeInStatus::PresignedFailed.to_string(), - current_time - INSTANCE_PRESIGNED_TIME_EXPIRED, + current_time - get_instance_presigned_time_expired_secs(), ) .await?; info!("Presigned expired instances is {expired_num}"); @@ -310,7 +310,7 @@ pub async fn instance_expiration_monitor( .await? }; - let lock_height = CONNECTOR_Z_TIMELOCK as i64; + let lock_height = default_connector_z_timelock_blocks(btc_client.network()) as i64; let mut storage_processor = local_db.acquire().await?; for instance in instances { if instance.btc_height > 0 && current_height > instance.btc_height + lock_height { diff --git a/node/src/utils.rs b/node/src/utils.rs index 8a432807..2b1c30f4 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -32,6 +32,7 @@ use bitvm_lib::actors::Actor; use bitvm_lib::committee::*; use bitvm_lib::keys::{OperatorMasterKey, VerifierMasterKey, WatchtowerMasterKey}; use bitvm_lib::operator::*; +use bitvm_lib::timelocks::{connector_f_timelock_blocks, default_timelock_config}; use bitvm_lib::types::{ BitvmGcCircuitData, BitvmGcGraph, BitvmGcGraphParameters, BitvmGcInstanceParameters, PrekickoffParameters, SimplifiedBitvmGcGraph, UserInfo, @@ -45,6 +46,7 @@ use goat::connectors::{ base::TaprootConnector, kickoff_connectors::{ForceSkipConnector, KickoffConnector, PrekickoffConnector}, }; +use goat::constants::TimelockConfig; use goat::contexts::base::generate_n_of_n_public_key; use goat::scripts::{generate_opreturn_script, p2a_output}; use goat::transactions::base::{Input, output_topology}; @@ -76,6 +78,7 @@ use crate::env; use crate::rpc_service::routes::v1::{ NODES_OPERATOR_BASE, NODES_WATCHTOWER_BASE, PROOFS_WATCHTOWER_PROOF_TIMEOUT, }; + use crate::scheduled_tasks::get_goat_message_content_type; use crate::scheduled_tasks::graph_maintenance_tasks::{ ChallengeSubStatus, VerifierChallengeStatus, @@ -137,18 +140,12 @@ pub mod todo_funcs { #![allow(dead_code, unreachable_code, unused_variables)] use super::*; + use bitvm_lib::timelocks::estimated_block_interval_secs; use bitvm_lib::types::SimplifiedBitvmGcGraph; // other operations pub fn avg_block_time_secs(network: Network) -> u64 { - match network { - Network::Bitcoin => 600, // 10 minutes - Network::Testnet => 300, // 5 minutes - Network::Testnet4 => 300, // 5 minutes - Network::Regtest => 60, // 1 minute - Network::Signet => 60, // 1 minute - // _ => 600, // default to 10 minutes - } + estimated_block_interval_secs(network) as u64 } pub fn min_required_operator() -> usize { // todo!("get min required operator number") @@ -989,6 +986,35 @@ async fn detect_connector_d_disprove( })) } +async fn detect_watchtower_flow_disprove( + btc_client: &BTCClient, + graph: &BitvmGcGraph, +) -> Result> { + for (index, nack) in graph.operator_challenge_nacks.iter().enumerate() { + let txid = nack.tx().compute_txid(); + if tx_on_chain(btc_client, &txid).await? { + return Ok(Some(DetectedDisprove { + disprove_type: DisproveTxType::OperatorChallengeNack, + index, + challenge_start_txid: None, + challenge_finish_txid: txid, + })); + } + } + + let txid = graph.operator_commit_timeout.tx().compute_txid(); + if tx_on_chain(btc_client, &txid).await? { + return Ok(Some(DetectedDisprove { + disprove_type: DisproveTxType::OperatorCommitTimeout, + index: 0, + challenge_start_txid: None, + challenge_finish_txid: txid, + })); + } + + Ok(None) +} + async fn scan_graph_chain_state( btc_client: &BTCClient, goat_client: &GOATClient, @@ -1179,6 +1205,19 @@ async fn scan_graph_chain_state( }); } + if let Some(disprove) = detect_watchtower_flow_disprove(btc_client, graph).await? { + sub_status.disprove_type = Some(disprove.disprove_type); + sub_status.disprove_index = disprove.index as i32; + return Ok(GraphChainScan { + status: GraphStatus::Disprove, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: Some(disprove), + }); + } + let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); watchtower_challenge_init_on_chain = tx_on_chain(btc_client, &watchtower_challenge_init_txid).await?; @@ -1241,6 +1280,19 @@ async fn scan_graph_chain_state( }); } + if let Some(disprove) = detect_watchtower_flow_disprove(btc_client, graph).await? { + sub_status.disprove_type = Some(disprove.disprove_type); + sub_status.disprove_index = disprove.index as i32; + return Ok(GraphChainScan { + status: GraphStatus::Disprove, + sub_status, + challenge_txid, + watchtower_challenge_init_on_chain, + operator_assert_on_chain, + disprove: Some(disprove), + }); + } + // TBD: add GraphStatus::Assert let mut detected_disprove = None; for verifier_index in 0..verifier_num { @@ -1768,8 +1820,12 @@ pub async fn read_instance_info_from_goat( }) } -pub async fn is_take1_timelock_expired(client: &BTCClient, kickoff_height: u32) -> Result { - let lock_blocks = take1_timelock(get_network()); +pub async fn is_take1_timelock_expired( + client: &BTCClient, + kickoff_height: u32, + timelock_config: &TimelockConfig, +) -> Result { + let lock_blocks = take1_timelock_with_config(get_network(), timelock_config); let current_height = client.get_height().await?; Ok(current_height >= kickoff_height + lock_blocks) } @@ -1777,10 +1833,15 @@ pub async fn is_take1_timelock_expired(client: &BTCClient, kickoff_height: u32) pub async fn is_take2_timelock_expired( client: &BTCClient, operator_assert_height: u32, + watchtower_challenge_init_height: u32, + timelock_config: &TimelockConfig, ) -> Result { - let lock_blocks = take2_timelock(get_network()); + let network = get_network(); + let connector_d_lock_blocks = take2_timelock_with_config(network, timelock_config); + let connector_f_lock_blocks = connector_f_timelock_blocks(network, timelock_config); let current_height = client.get_height().await?; - Ok(current_height >= operator_assert_height + lock_blocks) + Ok(current_height >= operator_assert_height + connector_d_lock_blocks + && current_height >= watchtower_challenge_init_height + connector_f_lock_blocks) } pub async fn get_fee_rate(client: &BTCClient) -> Result { @@ -1969,6 +2030,7 @@ pub async fn get_watchtower_commitment( pub async fn get_watchtower_challenge_info( btc_client: &BTCClient, watchtower_challenge_init_txid: &SerializableTxid, + watchtower_timeout_txids: &[Txid], num_watchtowers: usize, ) -> Result<(Vec>, Vec)> { let mut challenge_txids = Vec::with_capacity(num_watchtowers); @@ -1983,10 +2045,19 @@ pub async fn get_watchtower_challenge_info( Some(txid) => { let status = btc_client.get_tx_status(&txid).await?; if !status.confirmed { - bail!("watchtower challenge tx {txid} at index {index} is not confirmed yet"); + bail!("watchtower branch tx {txid} at index {index} is not confirmed yet"); + } + + let is_timeout = watchtower_timeout_txids + .get(index) + .is_some_and(|timeout_txid| txid == *timeout_txid); + if is_timeout { + challenge_txids.push(None); + included_watchtowers.push(false); + } else { + challenge_txids.push(Some(txid.to_string())); + included_watchtowers.push(true); } - challenge_txids.push(Some(txid.to_string())); - included_watchtowers.push(true); } None => { challenge_txids.push(None); @@ -2031,6 +2102,29 @@ pub async fn compute_operator_pubin_blockhash_and_bitmap( Ok((btc_best_block_hash, included_watchtowers)) } +async fn get_operator_committed_blockhash( + btc_client: &BTCClient, + challenge_txids: &[Option], + included_watchtowers_bits: &[bool], + graph_id: Uuid, +) -> Result> { + match compute_operator_pubin_blockhash_and_bitmap( + btc_client, + challenge_txids, + included_watchtowers_bits, + ) + .await + { + Ok((btc_best_block_hash, _)) => { + Ok(Some(BlockHash::from_byte_array(btc_best_block_hash).to_string())) + } + Err(e) => { + warn!("operator proof inputs are not ready for graph {graph_id}: {e}"); + Ok(None) + } + } +} + /// Assembles the 96-byte guest pubin: pub fn build_operator_guest_pubin( btc_best_block_hash: &[u8; 32], @@ -2124,9 +2218,12 @@ pub async fn get_operator_proof( .watchtower_challenge_init_txid .ok_or_else(|| anyhow::anyhow!("watchtower_challenge_init_txid is none"))?; let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); + let watchtower_timeout_txids: Vec = + bitvm_graph.watchtower_challenge_timeouts.iter().map(|tx| tx.tx().compute_txid()).collect(); let (watchtower_challenge_txids, included_watchtowers) = match get_watchtower_challenge_info( btc_client, &watchtower_challenge_init_txid, + &watchtower_timeout_txids, num_challenger, ) .await @@ -2137,13 +2234,16 @@ pub async fn get_operator_proof( return Ok((None, get_operator_proof_wait_secs())); } }; - let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( + let Some(operator_committed_blockhash) = get_operator_committed_blockhash( btc_client, &watchtower_challenge_txids, &included_watchtowers, + graph_id, ) - .await?; - let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); + .await? + else { + return Ok((None, get_operator_proof_wait_secs())); + }; let base_url = Url::parse( &get_proof_build_rpc_host() @@ -2207,7 +2307,10 @@ pub async fn get_operator_proof( let ark_proof = convert_ark_imm_wrap_vk(&proof, &proof_data.vk, &IMM_GROTH16_VK_BYTES, &part_stark_vk) .map_err(|e| anyhow!("failed to convert operator proof to ark format: {e}"))?; - if ark_proof.public_inputs[0] != statement.static_input { + let Some(static_input) = ark_proof.public_inputs.first() else { + bail!("operator proof has no public inputs"); + }; + if *static_input != statement.static_input { bail!("operator proof static public input does not match graph setup statement"); } @@ -2852,6 +2955,7 @@ pub async fn build_graph_params( graph_id: Uuid, ) -> Result { let instance_id = instance_parameters.instance_id; + let network = instance_parameters.network; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let operator_master_keypair = operator_master_key.master_keypair(); let operator_pubkey = operator_master_keypair.public_key().into(); @@ -2884,6 +2988,7 @@ pub async fn build_graph_params( Ok(BitvmGcGraphParameters { instance_parameters, prekickoff_parameters, + timelock_config: default_timelock_config(network), graph_id, graph_nonce, challenge_amount: todo_funcs::challenge_amount(), @@ -3131,6 +3236,32 @@ pub async fn endorse_pegin( Ok(sig) } +pub async fn verify_pegin_endorsement( + goat_client: &GOATClient, + instance_id: Uuid, + committee_pubkey: &PublicKey, + pegin_txid: &Txid, + signature: &[u8], +) -> Result { + let pegin_data = goat_client.gateway_get_pegin_data(&instance_id).await?; + let expected_evm_address = pegin_data + .committee_pubkeys + .iter() + .zip(pegin_data.committee_addresses.iter()) + .find_map(|(pubkey, evm_address)| { + PublicKey::from_slice(pubkey) + .ok() + .filter(|on_chain_pubkey| on_chain_pubkey == committee_pubkey) + .map(|_| *evm_address) + }) + .ok_or_else(|| anyhow!("committee pubkey {committee_pubkey} not found on-chain"))?; + let pegin_digest = goat_client.gateway_get_post_pegin_digest(&instance_id, pegin_txid).await?; + let sig = EvmSignature::try_from(signature)?; + sig.recover_address_from_prehash(&pegin_digest.into()) + .map(|addr| addr == expected_evm_address) + .map_err(|e| e.into()) +} + pub async fn verify_graph_endorsement( goat_client: &GOATClient, evm_address: &EvmAddress, @@ -3643,6 +3774,32 @@ pub struct GraphProcessDataItem { } pub type GraphProcessDataMap = IndexMap; +pub fn order_committee_values( + committee_pubkeys: &[PublicKey], + values: Vec<(PublicKey, T)>, + label: &str, +) -> Result> { + for (idx, (pubkey, _)) in values.iter().enumerate() { + if !committee_pubkeys.contains(pubkey) { + bail!("{label} contains non-committee pubkey {pubkey}"); + } + if values.iter().skip(idx + 1).any(|(other, _)| other == pubkey) { + bail!("{label} contains duplicate pubkey {pubkey}"); + } + } + + committee_pubkeys + .iter() + .map(|committee_pubkey| { + values + .iter() + .find(|(pubkey, _)| pubkey == committee_pubkey) + .map(|(_, value)| value.clone()) + .ok_or_else(|| anyhow!("{label} missing pubkey {committee_pubkey}")) + }) + .collect() +} + // db operations pub async fn get_current_prekickoff_tx( local_db: &LocalDB, @@ -3764,6 +3921,22 @@ pub async fn store_instance_parameters( instance_params: &BitvmGcInstanceParameters, ) -> Result<()> { let mut storage_processor = local_db.acquire().await?; + let incoming_parameters_hash = instance_params.parameters_hash()?; + if let Some(existing_parameters) = + storage_processor.get_instance_parameters_by_id(&instance_params.instance_id).await? + { + let existing_instance_params: BitvmGcInstanceParameters = + serde_json::from_str(&existing_parameters)?; + let existing_parameters_hash = existing_instance_params.parameters_hash()?; + if existing_parameters_hash != incoming_parameters_hash { + bail!(SpecialError::InvalidPeginData(format!( + "instance parameters changed for instance_id {}: existing={}, incoming={}", + instance_params.instance_id, + hex::encode(existing_parameters_hash), + hex::encode(incoming_parameters_hash) + ))); + } + } storage_processor .update_instance_parameters( &instance_params.instance_id, @@ -3862,6 +4035,29 @@ pub async fn store_graph(local_db: &LocalDB, simple_graph: &SimplifiedBitvmGcGra let instance_id = simple_graph.parameters.instance_parameters.instance_id; let current_time = current_time_secs(); let mut graph = convert_graph(&bitvm_graph, current_time); + let incoming_parameters_hash = simple_graph.parameters_hash()?; + + if let Some(existing_raw_data) = tx.find_graph_raw_data(&graph_id).await? { + let existing_graph = parse_graph_raw_data(existing_raw_data.raw_data, graph_id).await?; + let existing_parameters_hash = existing_graph.parameters_hash()?; + if existing_parameters_hash != incoming_parameters_hash { + bail!(SpecialError::InvalidGraph(format!( + "graph parameters changed for graph_id {graph_id}: existing={}, incoming={}", + hex::encode(existing_parameters_hash), + hex::encode(incoming_parameters_hash) + ))); + } + if existing_graph.operator_pre_signed() && !simple_graph.operator_pre_signed() { + bail!(SpecialError::InvalidGraph(format!( + "graph {graph_id} cannot be downgraded after operator pre-signatures are stored" + ))); + } + if existing_graph.committee_pre_signed() && !simple_graph.committee_pre_signed() { + bail!(SpecialError::InvalidGraph(format!( + "graph {graph_id} cannot be downgraded after committee pre-signatures are stored" + ))); + } + } if let Some(node_info) = tx.get_node_by_btc_pub_key(&bitvm_graph.parameters.operator_pubkey.to_string()).await? @@ -4115,6 +4311,14 @@ pub async fn store_committee_pub_nonces_for_graph( let mut storage_processor = local_db.acquire().await?; let (is_endorsed, mut process_data) = find_pegin_graph_process_data(&mut storage_processor, graph_id).await?; + if let Some(existing_pub_nonces) = + process_data.get(&committee_pubkey).and_then(|v| v.committee_pub_nonce.as_ref()) + && existing_pub_nonces != &pub_nonces + { + bail!(SpecialError::InvalidGraph(format!( + "committee pub nonces changed for graph {graph_id} and committee {committee_pubkey}" + ))); + } process_data .entry(committee_pubkey) .and_modify(|v| v.committee_pub_nonce = Some(pub_nonces.clone())) @@ -4157,6 +4361,14 @@ pub async fn store_committee_partial_sigs_for_graph( let mut storage_processor = local_db.acquire().await?; let (is_endorsed, mut process_data) = find_pegin_graph_process_data(&mut storage_processor, graph_id).await?; + if let Some(existing_partial_sigs) = + process_data.get(&committee_pubkey).and_then(|v| v.partial_sigs.as_ref()) + && existing_partial_sigs != &partial_sigs + { + bail!(SpecialError::InvalidGraph(format!( + "committee partial signatures changed for graph {graph_id} and committee {committee_pubkey}" + ))); + } process_data .entry(committee_pubkey) .and_modify(|v| v.partial_sigs = Some(partial_sigs.clone())) @@ -4189,6 +4401,18 @@ pub async fn get_committee_partial_sigs_for_graph( .filter_map(|(k, v)| v.partial_sigs.as_ref().map(|nonce| (*k, nonce.clone()))) .collect::>()) } + +pub async fn get_committee_partial_sigs_for_graph_member( + local_db: &LocalDB, + _instance_id: Uuid, + graph_id: Uuid, + committee_pubkey: &PublicKey, +) -> Result> { + let mut storage_processor = local_db.acquire().await?; + let (_is_endorsed, process_data) = + find_pegin_graph_process_data(&mut storage_processor, graph_id).await?; + Ok(process_data.get(committee_pubkey).and_then(|v| v.partial_sigs.clone())) +} pub async fn store_committee_endorsement_for_graph( local_db: &LocalDB, instance_id: Uuid, @@ -4300,6 +4524,14 @@ pub async fn store_committee_pub_nonce_for_instance( let mut storage_processor = local_db.acquire().await?; let mut process_data = find_pegin_instance_process_data(&mut storage_processor, instance_id).await?; + if let Some(existing_pub_nonce) = + process_data.get(&committee_pubkey).and_then(|v| v.pub_nonce.as_ref()) + && existing_pub_nonce != &pub_nonce + { + bail!(SpecialError::InvalidPeginData(format!( + "committee pub nonce changed for instance {instance_id} and committee {committee_pubkey}" + ))); + } process_data .entry(committee_pubkey) .and_modify(|v| v.pub_nonce = Some(pub_nonce.clone())) @@ -4342,6 +4574,14 @@ pub async fn store_committee_partial_sig_for_instance( let mut storage_processor = local_db.acquire().await?; let mut process_data = find_pegin_instance_process_data(&mut storage_processor, instance_id).await?; + if let Some(existing_partial_sig) = + process_data.get(&committee_pubkey).and_then(|v| v.partial_sign.as_ref()) + && existing_partial_sig != &partial_sigs + { + bail!(SpecialError::InvalidPeginData(format!( + "committee partial signature changed for instance {instance_id} and committee {committee_pubkey}" + ))); + } process_data .entry(committee_pubkey) .and_modify(|v| v.partial_sign = Some(partial_sigs)) @@ -4367,6 +4607,17 @@ pub async fn get_committee_partial_sigs_for_instance( .collect()) } +pub async fn get_committee_partial_sig_for_instance( + local_db: &LocalDB, + instance_id: Uuid, + committee_pubkey: &PublicKey, +) -> Result> { + let mut storage_processor = local_db.acquire().await?; + let process_data = + find_pegin_instance_process_data(&mut storage_processor, instance_id).await?; + Ok(process_data.get(committee_pubkey).and_then(|v| v.partial_sign)) +} + pub async fn store_committee_endorse_sig_for_pegin( local_db: &LocalDB, instance_id: Uuid, @@ -4376,6 +4627,15 @@ pub async fn store_committee_endorse_sig_for_pegin( let mut storage_processor = local_db.acquire().await?; let mut process_data = find_pegin_instance_process_data(&mut storage_processor, instance_id).await?; + if let Some(existing_endorse_sig) = + process_data.get(&committee_pubkey).map(|v| v.endorse_signature.as_slice()) + && !existing_endorse_sig.is_empty() + && existing_endorse_sig != endorse_sig.as_slice() + { + bail!(SpecialError::InvalidPeginData(format!( + "committee endorse signature changed for instance {instance_id} and committee {committee_pubkey}" + ))); + } process_data .entry(committee_pubkey) .and_modify(|v| v.endorse_signature = endorse_sig.clone()) @@ -4923,15 +5183,35 @@ mod commit_pubin_tests { let challenge_txid_wt0 = make_txid(0x01); let block_hash = make_block_hash(0xAA); - // watchtower 0 spent vout 0 of the init tx; watchtower 1 did not spend vout 1 + let challenge_vout_0 = + output_topology::watchtower_challenge_init::watchtower_connector(0) as u32; + let ack_vout_0 = output_topology::watchtower_challenge_init::ack_connector(0) as u32; + + // watchtower 0 spent its challenge connector and ACK connector; watchtower 1 is unresolved. mock_adaptor.set_tx( challenge_txid_wt0, - create_confirmed_tx(challenge_txid_wt0, &[(watchtower_init_txid, 0)], 100, block_hash), + create_confirmed_tx( + challenge_txid_wt0, + &[(watchtower_init_txid, challenge_vout_0)], + 100, + block_hash, + ), + ); + let ack_txid_wt0 = make_txid(0x02); + mock_adaptor.set_tx( + ack_txid_wt0, + create_confirmed_tx( + ack_txid_wt0, + &[(watchtower_init_txid, ack_vout_0)], + 101, + block_hash, + ), ); let (txids, bits) = get_watchtower_challenge_info( &btc_client, &SerializableTxid::from(watchtower_init_txid), + &[], 2, // 2 watchtowers ) .await @@ -4951,21 +5231,33 @@ mod commit_pubin_tests { let block_hash_low = make_block_hash(0x10); let block_hash_high = make_block_hash(0x20); + let challenge_vout_0 = + output_topology::watchtower_challenge_init::watchtower_connector(0) as u32; + let challenge_vout_2 = + output_topology::watchtower_challenge_init::watchtower_connector(2) as u32; + // wt0 confirmed at height 100, wt2 at height 200 (highest) mock_adaptor.set_tx( challenge_txid_wt0, - create_confirmed_tx(challenge_txid_wt0, &[(init_txid, 0)], 100, block_hash_low), + create_confirmed_tx( + challenge_txid_wt0, + &[(init_txid, challenge_vout_0)], + 100, + block_hash_low, + ), ); mock_adaptor.set_tx( challenge_txid_wt2, - create_confirmed_tx(challenge_txid_wt2, &[(init_txid, 2)], 200, block_hash_high), + create_confirmed_tx( + challenge_txid_wt2, + &[(init_txid, challenge_vout_2)], + 200, + block_hash_high, + ), ); - let challenge_txids = vec![ - Some(challenge_txid_wt0.to_string()), - None, - Some(challenge_txid_wt2.to_string()), - ]; + let challenge_txids = + vec![Some(challenge_txid_wt0.to_string()), None, Some(challenge_txid_wt2.to_string())]; let bits = vec![true, false, true]; let (best_hash, bitmap) = From c3303e4ef80f62b40eada0a815c90cb82a5c7921 Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Sun, 28 Jun 2026 11:56:14 +0800 Subject: [PATCH 07/14] Update operator assert logic (#409) * update operator commit witness * update handle * handle invalid assert extra-data * update goat deps --------- Co-authored-by: ethan --- Cargo.lock | 11 +-- Cargo.toml | 1 + crates/bitvm-gc/Cargo.toml | 1 + crates/bitvm-gc/src/babe_adapter.rs | 19 ++++- crates/bitvm-gc/src/operator/api.rs | 4 +- crates/bitvm-gc/src/timelocks.rs | 16 ++-- crates/bitvm-gc/src/verifier/api.rs | 116 +++++++++++++++++++++++++- crates/bitvm-gc/tests/babe_adapter.rs | 14 ++-- node/src/handle.rs | 99 ++++++++++++++-------- 9 files changed, 220 insertions(+), 61 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 8e04f22b..b2915a70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3005,7 +3005,7 @@ dependencies = [ [[package]] name = "bitvm" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#db6adf333ca594400c698ec60ad3a1f37811e421" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#fd8a985c16f39f0df26ad46749c266a3fb516d10" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -3040,6 +3040,7 @@ dependencies = [ "anyhow", "ark-bn254", "ark-crypto-primitives", + "ark-ec", "ark-ff 0.5.0", "ark-groth16", "ark-serialize 0.5.0", @@ -5576,7 +5577,7 @@ dependencies = [ [[package]] name = "garbled-snark-verifier" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#bcdca2a02b0a8ef80835df9304d285cf8aa37e28" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -5737,7 +5738,7 @@ dependencies = [ [[package]] name = "goat" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#db6adf333ca594400c698ec60ad3a1f37811e421" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#fd8a985c16f39f0df26ad46749c266a3fb516d10" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12202,7 +12203,7 @@ dependencies = [ [[package]] name = "soldering-host" version = "1.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#bcdca2a02b0a8ef80835df9304d285cf8aa37e28" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -13888,7 +13889,7 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#bcdca2a02b0a8ef80835df9304d285cf8aa37e28" dependencies = [ "aes", "ark-bn254", diff --git a/Cargo.toml b/Cargo.toml index f117e888..54b874f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -46,6 +46,7 @@ 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" diff --git a/crates/bitvm-gc/Cargo.toml b/crates/bitvm-gc/Cargo.toml index f84c8d57..d6e9e379 100644 --- a/crates/bitvm-gc/Cargo.toml +++ b/crates/bitvm-gc/Cargo.toml @@ -36,6 +36,7 @@ strum = { workspace = true, features = ["derive"] } [dev-dependencies] ark-crypto-primitives = { workspace = true } +ark-ec = { workspace = true } rand_chacha = { workspace = true } [features] diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 61840a9c..3be16b56 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -6,6 +6,7 @@ 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; @@ -354,7 +355,11 @@ pub fn build_assert_witness( } let msg = pi1_xd_to_wots96_msg(&proof.a, dynamic_input); let wots_sig = Wots96::sign(assert_secret_key, &msg); - Ok(TxAssertWitness { wots_sig: wots_sig.to_vec() }) + 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]> { @@ -366,6 +371,18 @@ pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> Ok(pi1_xd_to_wots96_msg(&pi1, x_d)) } +pub fn recover_operator_proof_from_assert_witness( + assert_witness: &TxAssertWitness, +) -> Result> { + let (pi1, _) = assert_witness + .recover_pi1_xd_without_verify() + .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, diff --git a/crates/bitvm-gc/src/operator/api.rs b/crates/bitvm-gc/src/operator/api.rs index 9d11b9ff..3d43a263 100644 --- a/crates/bitvm-gc/src/operator/api.rs +++ b/crates/bitvm-gc/src/operator/api.rs @@ -557,6 +557,7 @@ pub fn operator_sign_assert( graph: &mut BitvmGcGraph, wots_secret_key: &OperatorAssertSecretKey, proof: &[u8; 96], + extra_data: &[u8], ) -> Result { if Wots96::generate_public_key(wots_secret_key) != graph.parameters.operator_assert_wots_pubkey { @@ -564,10 +565,9 @@ pub fn operator_sign_assert( }; let connector_c = graph.connector_c(); - graph .operator_assert - .operator_commit_proof(wots_secret_key, &connector_c, proof) + .operator_commit_proof(wots_secret_key, &connector_c, proof, extra_data) .map_err(|e| anyhow::anyhow!("failed to sign operator assert: {e}"))?; Ok(graph.operator_assert.tx().clone()) } diff --git a/crates/bitvm-gc/src/timelocks.rs b/crates/bitvm-gc/src/timelocks.rs index 9e611ae3..a6570363 100644 --- a/crates/bitvm-gc/src/timelocks.rs +++ b/crates/bitvm-gc/src/timelocks.rs @@ -18,14 +18,14 @@ pub const NODE_BITCOIN_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { connector_f: 576, }; pub const NODE_TESTNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { - connector_z: 6, - connector_a: 6, - prover_connector: 12, - connector_d: 24, - watchtower_challenge: 24, - operator_ack: 36, - operator_commit: 48, - connector_f: 60, + connector_z: 100, + connector_a: 16, + prover_connector: 22, + connector_d: 34, + watchtower_challenge: 34, + operator_ack: 46, + operator_commit: 58, + connector_f: 70, }; pub const NODE_SIGNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { connector_z: 6, diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index c36d6d54..37f5ea03 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -1,12 +1,17 @@ use crate::{ + babe_adapter::TxAssertWitness, timelocks::{default_timelock_config, disprove_timelock_blocks}, types::BitvmGcGraph, }; use anyhow::{Result, bail}; -use bitcoin::{Address, Amount, Network, ScriptBuf, Transaction, TxIn, TxOut}; +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}, + 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}, @@ -16,6 +21,7 @@ use goat::{ 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 @@ -131,6 +137,108 @@ pub fn verify_prover_assertion(_graph: &BitvmGcGraph, _operator_assert_txin: TxI todo!("verify operator assertion") } +fn split_operator_assert_wots_and_extra_data( + mut operator_assertion: RawWitness, +) -> Result<(RawWitness, Vec)> { + let expected_len = PROVER_SIG_LEN + 1; + if operator_assertion.len() != expected_len { + bail!( + "operator assert witness has {} stack items; expected {expected_len}", + operator_assertion.len() + ); + } + let extra_data = operator_assertion + .pop() + .ok_or_else(|| anyhow::anyhow!("operator assert witness is empty"))?; + Ok((operator_assertion, extra_data)) +} + +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 split_operator_assert_extra_data(extra_data: Vec) -> Result<(Vec, Vec)> { + let pi2_len = G2Affine::default().compressed_size(); + let pi3_len = G1Affine::default().compressed_size(); + let expected_len = pi2_len + pi3_len; + if extra_data.len() != expected_len { + bail!("operator assert extra-data has {} bytes; expected {expected_len}", extra_data.len()); + } + Ok((extra_data[..pi2_len].to_vec(), extra_data[pi2_len..].to_vec())) +} + +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, extra_data) = + split_operator_assert_wots_and_extra_data(operator_assertion)?; + + let wots_sig = operator_assert_wots_signature(&operator_assertion)?; + let (pi2, pi3) = split_operator_assert_extra_data(extra_data)?; + 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, extra_data) = + split_operator_assert_wots_and_extra_data(operator_assertion)?; + let wots_sig = operator_assert_wots_signature(&operator_assertion)?; + let (pi2, pi3) = + split_operator_assert_extra_data(extra_data).unwrap_or_else(|_| (Vec::new(), Vec::new())); + Ok(TxAssertWitness { wots_sig, pi2, pi3 }) +} + pub fn build_verifier_assert_tx( graph: &BitvmGcGraph, operator_assert_txin: TxIn, @@ -145,6 +253,8 @@ pub fn build_verifier_assert_tx( 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, _extra_data) = + 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(); @@ -210,6 +320,8 @@ pub fn validate_pubin_disprove( 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, _extra_data) = + 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); diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index 3d1d809f..c3af5cdf 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -1,5 +1,6 @@ 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::{ @@ -9,7 +10,8 @@ use bitvm_gc::babe_adapter::{ 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, + recover_operator_proof_from_assert_witness, sample_cac_instance_commit, + sample_finalized_instance_data, verify_real_setup, verify_setup, }; use rand::SeedableRng; use rand_chacha::ChaCha12Rng; @@ -194,8 +196,8 @@ fn witness_builders_validate_inputs_and_indices() { y: ark_bn254::Fq::from(2u64), infinity: false, }, - b: ark_bn254::G2Affine::default(), - c: ark_bn254::G1Affine::default(), + b: ark_bn254::G2Affine::generator(), + c: ark_bn254::G1Affine::generator(), }; let dynamic_input = Fr::from(0u64); @@ -303,7 +305,7 @@ fn witness_builders_reject_wrong_finalized_count() { finalized_indices: vec![0], // 1, not BABE_M_CC verifier_pubkey: verifier_pubkey(), }; - let dummy_assert = TxAssertWitness { wots_sig: vec![] }; + 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 @@ -334,8 +336,8 @@ fn assert_witness_preserves_pi1_and_dynamic_input() { y: ark_bn254::Fq::from(2u64), infinity: false, }, - b: ark_bn254::G2Affine::default(), - c: ark_bn254::G1Affine::default(), + b: ark_bn254::G2Affine::generator(), + c: ark_bn254::G1Affine::generator(), }; let assert_witness = build_assert_witness(&proof, &sk, dynamic_input).expect("assert witness"); diff --git a/node/src/handle.rs b/node/src/handle.rs index d7219c34..0bd345f5 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -11,7 +11,7 @@ use crate::soldering_payload_store::{ }; use crate::utils::*; use anyhow::{Context, Result, anyhow, bail}; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_serialize::CanonicalSerialize; use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; @@ -20,7 +20,8 @@ use bitvm_lib::babe_adapter::{ CACSetupPackage, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, TxAssertWitness, WOTS_SIG_COUNT, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, - extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, + extract_gc_circuit_data, open_real_setup_and_solder, + recover_operator_proof_from_assert_witness, recover_real_wrongly_challenged_witness, verify_real_setup, }; use bitvm_lib::committee::*; @@ -4020,6 +4021,10 @@ async fn handle_assert_ready_operator( let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; + let mut assert_extra_data = + Vec::with_capacity(assert_witness.pi2.len() + assert_witness.pi3.len()); + assert_extra_data.extend_from_slice(&assert_witness.pi2); + assert_extra_data.extend_from_slice(&assert_witness.pi3); let mut asserted_operator_proof = Vec::new(); operator_proof.proof.serialize_compressed(&mut asserted_operator_proof)?; let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? @@ -4036,8 +4041,11 @@ async fn handle_assert_ready_operator( operator_state.asserted_operator_proof = Some(asserted_operator_proof); save_babe_setup_state(ctx.local_db, instance_id, graph_id, &setup_state)?; - let assert_tx = operator_sign_assert(&mut graph, &assert_secret_key, &assert_message)?; - broadcast_nonstandard_tx(ctx.btc_client, &assert_tx).await?; + let assert_tx = + operator_sign_assert(&mut graph, &assert_secret_key, &assert_message, &assert_extra_data)?; + let assert_tx_total_input_amount = + graph.operator_assert.prev_outs().iter().map(|o| o.value).sum::(); + broadcast_tx_with_cpfp(ctx.btc_client, assert_tx, assert_tx_total_input_amount).await?; Ok(()) } @@ -4189,6 +4197,11 @@ async fn handle_assert_sent_verifier( ) { Ok(Some((witness_data, _))) => { // build pubin disprove Tx + let pubin_disprove_tx_total_input_amount = graph + .operator_assert + .connector_d_input() + .map_err(|e| anyhow!("failed to get connector-d input: {e}"))? + .amount; let pubin_disprove_txin = build_pubin_disprove_txin(&graph, witness_data)?; let pubin_disprove_tx = bitcoin::Transaction { @@ -4197,7 +4210,12 @@ async fn handle_assert_sent_verifier( input: vec![pubin_disprove_txin], output: vec![goat::scripts::p2a_output()], }; - broadcast_nonstandard_tx(ctx.btc_client, &pubin_disprove_tx).await?; + broadcast_tx_with_cpfp( + ctx.btc_client, + pubin_disprove_tx, + pubin_disprove_tx_total_input_amount, + ) + .await?; return Ok(()); } Ok(None) => tracing::debug!( @@ -4225,22 +4243,21 @@ async fn handle_assert_sent_verifier( } } - // recover TxAssertWitness from the assert tx witness and graph. - let raw_witness = graph - .connector_c() - .extract_leaf_1_raw_witness( - assert_tx - .input - .first() - .ok_or_else(|| anyhow!("operator assert transaction has no input"))?, - ) - .map_err(|e| anyhow!("failed to extract WOTS signature from assert tx: {e}"))?; - let mut bitcoin_witness = bitcoin::Witness::new(); - for item in &raw_witness { - bitcoin_witness.push(item); + // recover TxAssertWitness from the assert tx witness. + let assert_txin = assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; + let assert_witness = extract_operator_assert_witness_for_challenge(&graph, assert_txin) + .map_err(|e| anyhow!("failed to extract operator assert witness: {e}"))?; + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for operator assert")?; + let static_input = derive_operator_static_input()?; + if assert_witness.verify_groth16_proof(&vk, &[static_input]) { + tracing::info!( + "Skip ChallengeAssert for {instance_id}:{graph_id}: operator assert proof is valid" + ); + return Ok(()); } - let assert_witness = - TxAssertWitness { wots_sig: Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec() }; let Some(saved_verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .and_then(|state| state.verifier) @@ -4256,8 +4273,6 @@ async fn handle_assert_sent_verifier( ); return Ok(()); } - let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE challenge")?; - let static_input = derive_operator_static_input()?; let challenge_witness = build_real_challenge_assert_witness( &saved_verifier_state.private_state, &saved_verifier_state.setup_package, @@ -4289,7 +4304,14 @@ async fn handle_assert_sent_verifier( .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; let challenge_assert_tx = build_verifier_assert_tx(&graph, operator_assert_txin, verifier_index, labels)?; - broadcast_nonstandard_tx(ctx.btc_client, &challenge_assert_tx).await?; + let challenge_assert_tx_total_input_amount = + graph.verifier_asserts[verifier_index].prev_outs().iter().map(|o| o.value).sum::(); + broadcast_tx_with_cpfp( + ctx.btc_client, + challenge_assert_tx, + challenge_assert_tx_total_input_amount, + ) + .await?; Ok(()) } @@ -4356,8 +4378,10 @@ async fn handle_challenge_assert_sent_operator( .first() .cloned() .ok_or_else(|| anyhow!("published operator assert transaction has no input"))?; + let operator_assert_witness = extract_operator_assert_witness(&graph, &operator_assert_txin) + .map_err(|e| anyhow!("failed to extract operator assert witness: {e}"))?; let expected_challenge_assert = - build_verifier_assert_tx(&graph, operator_assert_txin, verifier_index, labels)?; + build_verifier_assert_tx(&graph, operator_assert_txin.clone(), verifier_index, labels)?; if expected_challenge_assert.input.first().map(|input| &input.witness) != challenge_assert_tx.input.first().map(|input| &input.witness) { @@ -4378,20 +4402,19 @@ async fn handle_challenge_assert_sent_operator( .prover_state .as_ref() .ok_or_else(|| anyhow!("missing BABE prover state for verifier slot {verifier_index}"))?; - let proof_bytes = operator_state - .asserted_operator_proof - .as_ref() - .ok_or_else(|| anyhow!("missing asserted operator proof for graph {graph_id}"))?; - let proof = Groth16Proof::deserialize_compressed(proof_bytes.as_slice()) - .context("deserialize asserted operator proof")?; + let assert_witness = TxAssertWitness { + wots_sig: challenge_witness.witness.wots_sig.clone(), + pi2: operator_assert_witness.pi2, + pi3: operator_assert_witness.pi3, + }; + let proof = recover_operator_proof_from_assert_witness(&assert_witness) + .context("recover asserted operator proof from on-chain assert witness")?; let vk = crate::vk::get_vk() .await .context("load Groth16 verifying key for BABE wrongly challenged")?; - let (_, dyn_pubin) = TxAssertWitness { wots_sig: challenge_witness.witness.wots_sig.clone() } - .recover_pi1_xd_without_verify() - .ok_or_else(|| { - anyhow!("cannot recover dynamic input from challenge witness WOTS signature") - })?; + let (_, dyn_pubin) = assert_witness.recover_pi1_xd_without_verify().ok_or_else(|| { + anyhow!("cannot recover dynamic input from challenge witness WOTS signature") + })?; let wrongly_challenged_witness = recover_real_wrongly_challenged_witness( prover_state, &challenge_witness, @@ -4411,7 +4434,7 @@ async fn handle_challenge_assert_sent_operator( input: vec![wrongly_challenged_input], output: vec![goat::scripts::p2a_output()], }; - broadcast_nonstandard_tx(ctx.btc_client, &wrongly_challenged_tx).await?; + broadcast_tx(ctx.btc_client, &wrongly_challenged_tx).await?; Ok(()) } @@ -4502,7 +4525,9 @@ async fn handle_wrongly_challenge_timeout_verifier( } let disprove_tx = build_disprove_tx(&graph, verifier_index, None)?; - broadcast_nonstandard_tx(ctx.btc_client, &disprove_tx).await?; + let disprove_tx_total_input_amount = + graph.disproves[verifier_index].prev_outs().iter().map(|o| o.value).sum::(); + broadcast_tx_with_cpfp(ctx.btc_client, disprove_tx, disprove_tx_total_input_amount).await?; Ok(()) } From 786c9a5130d22078f5e503e8e755a910e65268a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Gia=20Vi=E1=BB=87t=20Anh?= Date: Wed, 1 Jul 2026 22:15:11 +0700 Subject: [PATCH 08/14] Feat: validate assert witness in handle sent verifier. (#410) * adapt babe to newest change in gcs * change deps --- Cargo.lock | 6 +- crates/bitvm-gc/src/babe_adapter.rs | 91 +++++++++------------------ crates/bitvm-gc/tests/babe_adapter.rs | 39 +++++++++--- node/src/handle.rs | 3 +- 4 files changed, 61 insertions(+), 78 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b2915a70..a525d844 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5577,7 +5577,7 @@ dependencies = [ [[package]] name = "garbled-snark-verifier" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#bcdca2a02b0a8ef80835df9304d285cf8aa37e28" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#d1a69cf194e04fc413c6cdd1e8d47b4e834c4ddc" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12203,7 +12203,7 @@ dependencies = [ [[package]] name = "soldering-host" version = "1.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#bcdca2a02b0a8ef80835df9304d285cf8aa37e28" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#d1a69cf194e04fc413c6cdd1e8d47b4e834c4ddc" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -13889,7 +13889,7 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#bcdca2a02b0a8ef80835df9304d285cf8aa37e28" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#d1a69cf194e04fc413c6cdd1e8d47b4e834c4ddc" dependencies = [ "aes", "ark-bn254", diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 3be16b56..225c161f 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -11,7 +11,7 @@ use ark_groth16::VerifyingKey as Groth16VerifyingKey; use ark_serialize::CanonicalSerialize; use garbled_snark_verifier::bag::S; use goat::assert_scripts::{ - INPUT_WIRE_NUM, OperatorAssertPublicKey, OperatorAssertSecretKey, WireHash, label_hash, + INPUT_WIRE_NUM, OperatorAssertSecretKey, WireHash, label_hash, }; use goat::wots::{Wots, Wots96}; use serde::{Deserialize, Serialize}; @@ -19,9 +19,9 @@ use sha2::{Digest, Sha256}; use soldering_host::BabeBundle; pub use soldering_host::BabeBundleBuilder; use verifiable_circuit_babe::babe::{ - GC_INPUT_WIRES, WeKnownPi1SetupCt, build_challenge_assert_witness_raw, - deinterleave_dummy_positions, interleave_dummy_positions, + GC_INPUT_WIRES, WeKnownPi1SetupCt, build_challenge_assert_witness as babe_build_challenge_assert_witness, }; +use verifiable_circuit_babe::dre::N_PADDED; use verifiable_circuit_babe::cac::cac_finalize_indices; pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; use verifiable_circuit_babe::gc::{SGC_PART1_CONSTANT_SIZE, SparseAdaptorTable}; @@ -317,7 +317,7 @@ pub fn verify_setup( /// Extracts one graph slot owned by `verifier_pubkey` from finalized setup data. /// -/// `epk` must have exactly `GC_INPUT_WIRES` (762) entries +/// `epk` must have exactly `GC_INPUT_WIRES` (768) entries pub fn extract_gc_circuit_data( verifier_pubkey: bitcoin::PublicKey, epk: &[[[u8; 20]; 2]], @@ -326,19 +326,15 @@ pub fn extract_gc_circuit_data( if epk.len() != GC_INPUT_WIRES { bail!("BABE epk has {} entries; expected {GC_INPUT_WIRES}", epk.len()); } - let n = GC_INPUT_WIRES / 3; let to_wire_hash = |pair: &[[u8; 20]; 2]| WireHash { false_label_hash: pair[0], true_label_hash: pair[1] }; - let dummy_hash = label_hash(&vec![0u8; 16]); - let dummy = WireHash { false_label_hash: dummy_hash, true_label_hash: dummy_hash }; - let pi1_x: Vec = epk[..n].iter().map(to_wire_hash).collect(); - let pi1_y: Vec = epk[n..2 * n].iter().map(to_wire_hash).collect(); - let x_d: Vec = epk[2 * n..].iter().map(to_wire_hash).collect(); - let wire_hashes_vec = interleave_dummy_positions(&pi1_x, &pi1_y, &x_d, dummy); - - let wire_hashes: [WireHash; INPUT_WIRE_NUM] = - wire_hashes_vec.try_into().map_err(|v: Vec| { + 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 }) @@ -363,19 +359,21 @@ pub fn build_assert_witness( } pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> { - let recover = assert_witness.recover_pi1_xd_without_verify(); - if recover.is_none() { - return Err(anyhow::anyhow!("Cannot recover pi1 and xd")); - } - let (pi1, x_d) = recover.unwrap(); - Ok(pi1_xd_to_wots96_msg(&pi1, x_d)) + 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 - .recover_pi1_xd_without_verify() + .try_recover_pi1_xd() .ok_or_else(|| anyhow::anyhow!("Cannot recover pi1 and xd"))?; let (pi2, pi3) = assert_witness .recover_pi2_pi3() @@ -405,14 +403,12 @@ pub fn build_challenge_assert_witness( } /// Verifies a native operator assertion and reveals the real base-instance labels. -#[allow(clippy::too_many_arguments)] pub fn build_real_challenge_assert_witness( private_state: &BabeVerifierPrivateState, package: &CACSetupPackage, finalized_indices: &[usize], vk: &Groth16VerifyingKey, static_inputs: Fr, - operator_wots_pubkey: &OperatorAssertPublicKey, assert_witness: &TxAssertWitness, verifier_index: usize, ) -> Result { @@ -424,37 +420,16 @@ pub fn build_real_challenge_assert_witness( bail!("persisted BABE verifier state does not reproduce setup package"); } - let recover = assert_witness.recover_pi1_xd_without_verify(); - if recover.is_none() { - return Err(anyhow::anyhow!("Cannot recover pi1 and xd")); - } - let (pi1, x_d) = recover.unwrap(); - let expected_message = pi1_xd_to_wots96_msg(&pi1, x_d); - let wots_sig = to_real_wots_sig(&assert_witness.wots_sig)?; - let signed_message = Wots96::signature_to_message(&wots_sig); - if signed_message != expected_message { - bail!("operator BABE assertion WOTS signature message does not match pi1/pubin"); - } let base_idx = finalized_indices[0]; + let witness = babe_build_challenge_assert_witness(&verifier, assert_witness, base_idx); - let witness = build_challenge_assert_witness_raw( - &verifier, - assert_witness, - operator_wots_pubkey, - base_idx, - ); - if witness.is_none() { - bail!("cannot generate challenge assert witness"); - } - let witness = witness.unwrap(); - - if witness.input_labels.len() != INPUT_WIRE_NUM { + if witness.witness.input_labels.len() != GC_INPUT_WIRES { bail!( - "real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", - witness.input_labels.len() + "real BABE challenge labels have {}; expected {GC_INPUT_WIRES}", + witness.witness.input_labels.len() ); } - Ok(BabeChallengeAssertWitness { verifier_index, witness }) + Ok(BabeChallengeAssertWitness { verifier_index, witness: witness.witness }) } /// Builds a wrongly-challenged witness from a valid recovered finalized-message preimage. @@ -613,9 +588,8 @@ fn recover_a_valid_finalized_messages( let base_input_labels: Vec = challenge_witness.witness.input_labels.iter().map(|&b| S(b)).collect(); - // Strip the 6 dummy labels before passing to the GC (which has GC_INPUT_WIRES real wires). - let (pi1_x_labels, pi1_y_labels, x_d_labels) = deinterleave_dummy_positions(&base_input_labels); - let pi1_labels: Vec = pi1_x_labels.into_iter().chain(pi1_y_labels).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; @@ -628,8 +602,8 @@ fn recover_a_valid_finalized_messages( let found = prover.check_compute_msg( &prover_state.finalized, - &pi1_labels, - &x_d_labels, + &pi1_full_labels, + &x_d_full_labels, &real_soldering, &prover_state.h_msgs, ); @@ -649,15 +623,6 @@ fn recover_a_valid_finalized_messages( }) } -fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature> { - wots_sig.try_into().map_err(|_| { - anyhow::anyhow!( - "WOTS signature has {} digit signatures; expected {WOTS_SIG_COUNT}", - wots_sig.len() - ) - }) -} - fn deterministic_seed(index: usize) -> u64 { u64::from_le_bytes(hash32(&(index as u64).to_le_bytes())[0..8].try_into().expect("8 bytes")) } diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index c3af5cdf..193a490e 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -10,7 +10,7 @@ use bitvm_gc::babe_adapter::{ 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, - recover_operator_proof_from_assert_witness, sample_cac_instance_commit, + sample_cac_instance_commit, sample_finalized_instance_data, verify_real_setup, verify_setup, }; use rand::SeedableRng; @@ -72,7 +72,7 @@ fn real_setup_restores_private_state_and_verifies_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 508-wire graph data"); + .expect("extract native 768-wire graph data"); } #[test] @@ -118,21 +118,21 @@ fn protocol_finalized_instances_contribute_one_base_wire_slot() { // M finalized instances each contribute one hashlock … assert_eq!(gc_data.final_msg_hashlocks, h_msgs); - let n = GC_INPUT_WIRES / 3; // 254 real wires per group - // pi1_x: positions 0..n + 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 + 2].false_label_hash, epk[n][0]); - assert_eq!(gc_data.wire_hashes[2 * (n + 2)].false_label_hash, epk[2 * n][0]); - assert_eq!(gc_data.wire_hashes[2 * (n + 2) + n - 1].false_label_hash, epk[3 * 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, INPUT_WIRE_NUM] { + 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"), @@ -204,7 +204,7 @@ fn witness_builders_validate_inputs_and_indices() { 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.recover_pi1_xd_without_verify().is_some()); + assert!(assert_witness.try_recover_pi1_xd().is_some()); assert!(build_assert_witness(&proof, &Vec::new(), dynamic_input).is_err()); let verifier_state = BabeVerifierState { @@ -343,7 +343,7 @@ fn assert_witness_preserves_pi1_and_dynamic_input() { let assert_witness = build_assert_witness(&proof, &sk, dynamic_input).expect("assert witness"); let (recovered_pi1, recovered_xd) = - assert_witness.recover_pi1_xd_without_verify().expect("recover pi1 and xd"); + assert_witness.try_recover_pi1_xd().expect("recover pi1 and xd"); assert_eq!(recovered_pi1, proof.a); assert_eq!(recovered_xd, dynamic_input); @@ -352,3 +352,22 @@ fn assert_witness_preserves_pi1_and_dynamic_input() { 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_gc::babe_adapter::{TxAssertWitness, assert_wots_message}; + use goat::wots::Wots96; + use bitvm::signatures::Wots; + + // 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/node/src/handle.rs b/node/src/handle.rs index 0bd345f5..12eab382 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -4279,7 +4279,6 @@ async fn handle_assert_sent_verifier( &saved_verifier_state.finalized_indices, &vk, static_input, - &graph.parameters.operator_assert_wots_pubkey, &assert_witness, verifier_index, )?; @@ -4412,7 +4411,7 @@ async fn handle_challenge_assert_sent_operator( let vk = crate::vk::get_vk() .await .context("load Groth16 verifying key for BABE wrongly challenged")?; - let (_, dyn_pubin) = assert_witness.recover_pi1_xd_without_verify().ok_or_else(|| { + let (_, dyn_pubin) = assert_witness.try_recover_pi1_xd().ok_or_else(|| { anyhow!("cannot recover dynamic input from challenge witness WOTS signature") })?; let wrongly_challenged_witness = recover_real_wrongly_challenged_witness( From 3893de9ef76ffcdf63f1d894f35abdcba86ff45a Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Thu, 2 Jul 2026 00:40:29 +0800 Subject: [PATCH 09/14] Fix some problems (#411) * fix verifier check * fix sync graph check * fix endorse sig check * add check for graph sigs * update graph verify logic * fix nonstandard witness * update deps --------- Co-authored-by: ethan --- Cargo.lock | 22 +-- crates/bitvm-gc/src/committee/api.rs | 141 ++++++++++++++- crates/bitvm-gc/src/operator/api.rs | 55 +++++- crates/bitvm-gc/src/verifier/api.rs | 40 +++-- crates/client/src/goat_chain/chain_adaptor.rs | 2 +- node/src/handle.rs | 138 +++++++++++++-- node/src/utils.rs | 167 +++++++++++++----- 7 files changed, 472 insertions(+), 93 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a525d844..93de5988 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1325,7 +1325,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1336,7 +1336,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3005,7 +3005,7 @@ dependencies = [ [[package]] name = "bitvm" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#fd8a985c16f39f0df26ad46749c266a3fb516d10" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#e369b2a59d86dad2bdc52daeb5fc6b890e9dc812" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -4825,7 +4825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5738,7 +5738,7 @@ dependencies = [ [[package]] name = "goat" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#fd8a985c16f39f0df26ad46749c266a3fb516d10" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#e369b2a59d86dad2bdc52daeb5fc6b890e9dc812" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -8183,7 +8183,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11308,7 +11308,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11411,7 +11411,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12168,7 +12168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12854,7 +12854,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -14259,7 +14259,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/crates/bitvm-gc/src/committee/api.rs b/crates/bitvm-gc/src/committee/api.rs index d92b3b0e..dfbcc8b2 100644 --- a/crates/bitvm-gc/src/committee/api.rs +++ b/crates/bitvm-gc/src/committee/api.rs @@ -4,7 +4,7 @@ use bitcoin::{ PublicKey, Script, TapLeafHash, TapSighash, TapSighashType, Transaction, TxOut, key::Keypair, sighash::{Prevouts, SighashCache}, - taproot::{LeafVersion, Signature as TaprootSignature}, + taproot::{ControlBlock, LeafVersion, Signature as TaprootSignature}, }; use goat::contexts::base::generate_n_of_n_public_key; use goat::transactions::base::BaseTransaction; @@ -13,7 +13,7 @@ use goat::transactions::pre_signed_musig2::{get_nonce_message, verify_public_non use goat::transactions::signing_musig2::generate_aggregated_nonce; use musig2::secp::Point; use musig2::{AggNonce, KeyAggContext, PartialSignature, PubNonce, SecNonce, verify_partial}; -use secp256k1::schnorr::Signature as SchnorrSignature; +use secp256k1::{Message, SECP256K1, schnorr::Signature as SchnorrSignature}; use serde::{Deserialize, Serialize}; use crate::keys::hkdf_derive_bytes; @@ -411,7 +411,10 @@ fn taproot_script_spend_sighash( prevouts: &[TxOut], leaf_hash: TapLeafHash, sighash_type: TapSighashType, -) -> TapSighash { +) -> 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 @@ -419,11 +422,11 @@ fn taproot_script_spend_sighash( SighashCache::new(tx) .taproot_script_spend_signature_hash( input_index, - &Prevouts::One(input_index, &prevouts[input_index]), + &Prevouts::One(input_index, prevout), leaf_hash, sighash_type, ) - .expect("Failed to construct sighash") + .map_err(|e| anyhow::anyhow!("failed to construct input {input_index} sighash: {e}")) } else { SighashCache::new(tx) .taproot_script_spend_signature_hash( @@ -432,7 +435,7 @@ fn taproot_script_spend_sighash( leaf_hash, sighash_type, ) - .expect("Failed to construct sighash") + .map_err(|e| anyhow::anyhow!("failed to construct input {input_index} sighash: {e}")) } } @@ -457,11 +460,91 @@ pub fn verify_taproot_partial_signature( ) -> 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); + 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, @@ -630,6 +713,50 @@ pub fn verify_graph_committee_partial_sigs( 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], diff --git a/crates/bitvm-gc/src/operator/api.rs b/crates/bitvm-gc/src/operator/api.rs index 3d43a263..563f080a 100644 --- a/crates/bitvm-gc/src/operator/api.rs +++ b/crates/bitvm-gc/src/operator/api.rs @@ -1,6 +1,6 @@ use anyhow::{Result, bail}; -use bitcoin::{Address, Amount, Transaction, TxIn, key::Keypair}; -use bitcoin::{Network, PublicKey, Witness}; +use bitcoin::{Address, Amount, TapSighashType, Transaction, TxIn, key::Keypair}; +use bitcoin::{Network, PublicKey, Witness, XOnlyPublicKey}; use goat::assert_scripts::{ OperatorAssertPublicKey, OperatorAssertSecretKey, OperatorCommitPubinPublicKey, OperatorCommitPubinSecretKey, @@ -26,6 +26,7 @@ use goat::transactions::watchtower_challenge::{ }; 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, @@ -393,6 +394,51 @@ pub fn push_operator_pre_signature( 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, @@ -557,7 +603,8 @@ pub fn operator_sign_assert( graph: &mut BitvmGcGraph, wots_secret_key: &OperatorAssertSecretKey, proof: &[u8; 96], - extra_data: &[u8], + pi2: &[u8], + pi3: &[u8], ) -> Result { if Wots96::generate_public_key(wots_secret_key) != graph.parameters.operator_assert_wots_pubkey { @@ -567,7 +614,7 @@ pub fn operator_sign_assert( let connector_c = graph.connector_c(); graph .operator_assert - .operator_commit_proof(wots_secret_key, &connector_c, proof, extra_data) + .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()) } diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 37f5ea03..c9b4b7c0 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -139,18 +139,21 @@ pub fn verify_prover_assertion(_graph: &BitvmGcGraph, _operator_assert_txin: TxI fn split_operator_assert_wots_and_extra_data( mut operator_assertion: RawWitness, -) -> Result<(RawWitness, Vec)> { - let expected_len = PROVER_SIG_LEN + 1; +) -> 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 extra_data = operator_assertion + let pi3 = operator_assertion .pop() .ok_or_else(|| anyhow::anyhow!("operator assert witness is empty"))?; - Ok((operator_assertion, extra_data)) + 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> { @@ -191,14 +194,16 @@ fn operator_assert_wots_signature(operator_assertion: &RawWitness) -> Result) -> Result<(Vec, Vec)> { +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(); - let expected_len = pi2_len + pi3_len; - if extra_data.len() != expected_len { - bail!("operator assert extra-data has {} bytes; expected {expected_len}", extra_data.len()); + 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((extra_data[..pi2_len].to_vec(), extra_data[pi2_len..].to_vec())) + Ok(()) } pub fn extract_operator_assert_witness( @@ -210,11 +215,11 @@ pub fn extract_operator_assert_witness( .connector_c() .extract_leaf_1_raw_witness(operator_assert_txin) .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; - let (operator_assertion, extra_data) = + 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) = split_operator_assert_extra_data(extra_data)?; + 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"); @@ -231,11 +236,14 @@ pub fn extract_operator_assert_witness_for_challenge( .connector_c() .extract_leaf_1_raw_witness(operator_assert_txin) .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; - let (operator_assertion, extra_data) = + 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) = - split_operator_assert_extra_data(extra_data).unwrap_or_else(|_| (Vec::new(), Vec::new())); + 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 }) } @@ -253,7 +261,7 @@ pub fn build_verifier_assert_tx( 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, _extra_data) = + let (operator_assertion, _pi2, _pi3) = split_operator_assert_wots_and_extra_data(operator_assertion)?; let verifier_connector = graph.verifier_connector(verifier_index)?; @@ -320,7 +328,7 @@ pub fn validate_pubin_disprove( 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, _extra_data) = + 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 = diff --git a/crates/client/src/goat_chain/chain_adaptor.rs b/crates/client/src/goat_chain/chain_adaptor.rs index f962d5bb..3fc6ac00 100644 --- a/crates/client/src/goat_chain/chain_adaptor.rs +++ b/crates/client/src/goat_chain/chain_adaptor.rs @@ -362,7 +362,7 @@ pub struct WithdrawData { pub btc_block_height_withdraw: U256, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct GraphData { pub operator_pubkey_prefix: u8, pub operator_pubkey: [u8; 32], diff --git a/node/src/handle.rs b/node/src/handle.rs index 12eab382..3802f977 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -622,6 +622,12 @@ fn freeze_operator_candidates(state: &mut OperatorBabeSetupState) -> Result<()> todo_funcs::min_required_verifier() ); } + let mut verifier_peer_ids = std::collections::HashSet::new(); + for candidate in &state.candidates { + if !verifier_peer_ids.insert(&candidate.verifier_peer_id) { + bail!("cannot freeze duplicate verifier peer id"); + } + } state.candidates.truncate(todo_funcs::min_required_verifier()); state.candidates.sort_by_key(|candidate| candidate.verifier_pubkey.to_bytes()); for (verifier_index, candidate) in state.candidates.iter_mut().enumerate() { @@ -1266,6 +1272,20 @@ async fn handle_gen_circuits_operator( bail!("GenCircuits setup package has no commitments"); } + let verifier_peer_id = ctx.from_peer_id.to_bytes(); + if !ctx.goat_client.committee_mana_is_verifier(&verifier_peer_id).await.with_context(|| { + format!( + "failed to validate GenCircuits sender {} against the verifier registry", + ctx.from_peer_id + ) + })? { + tracing::warn!( + "Ignore GenCircuits for {instance_id}:{graph_id}: sender {} is not a registered verifier", + ctx.from_peer_id + ); + return Ok(()); + } + let mut state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)?.unwrap_or_default(); let operator_state = state.operator.get_or_insert_with(|| OperatorBabeSetupState { frozen_verifier_pubkeys: None, @@ -1277,11 +1297,27 @@ async fn handle_gen_circuits_operator( if let Some(existing) = operator_state .candidates .iter() - .find(|candidate| candidate.verifier_pubkey == *verifier_pubkey) + .find(|candidate| candidate.verifier_peer_id == verifier_peer_id) { + if existing.verifier_pubkey != *verifier_pubkey { + tracing::warn!( + "Ignore GenCircuits for {instance_id}:{graph_id}: verifier peer {} already submitted a different public key", + ctx.from_peer_id + ); + return Ok(()); + } if existing.setup_package != *setup_package { bail!("conflicting GenCircuits setup package for verifier {verifier_pubkey}"); } + } else if operator_state + .candidates + .iter() + .any(|candidate| candidate.verifier_pubkey == *verifier_pubkey) + { + tracing::warn!( + "Ignore GenCircuits for {instance_id}:{graph_id}: verifier public key {verifier_pubkey} is already bound to another peer" + ); + return Ok(()); } else if was_frozen { tracing::debug!( "Ignore GenCircuits for {instance_id}:{graph_id}: verifier membership is frozen" @@ -1290,6 +1326,7 @@ async fn handle_gen_circuits_operator( return Ok(()); } else { operator_state.candidates.push(OperatorVerifierCandidate { + verifier_peer_id, verifier_pubkey: *verifier_pubkey, setup_package: setup_package.clone(), verifier_index: None, @@ -1497,6 +1534,14 @@ async fn handle_soldering_proof_ready_operator( if candidate.verifier_index != Some(verifier_index) { bail!("selected verifier candidate index does not match SolderingProofReady slot"); } + let verifier_peer_id = ctx.from_peer_id.to_bytes(); + if candidate.verifier_peer_id != verifier_peer_id { + tracing::warn!( + "Ignore SolderingProofReady for {instance_id}:{graph_id}: sender {} does not own verifier slot {verifier_index}", + ctx.from_peer_id + ); + return Ok(()); + } let store_base_path = get_soldering_proof_payload_store_path()?; let payload_path = soldering_proof_payload_store_path( @@ -1784,9 +1829,7 @@ async fn handle_create_graph_committee( ) -> Result<()> { // received from Operator // 1. check graph data & operator stake - if let Err(e) = - todo_funcs::validate_init_graph(ctx.local_db, ctx.btc_client, ctx.goat_client, graph).await - { + if let Err(e) = todo_funcs::validate_init_graph(ctx.btc_client, ctx.goat_client, graph).await { if should_ignore_invalid_graph(&e, instance_id, graph_id, "CreateGraph", None) { return Ok(()); } @@ -2438,7 +2481,9 @@ async fn handle_graph_finalize_committee( ) -> Result<()> { // received from Operator // 1. check graph data - if let Err(e) = todo_funcs::validate_finalized_graph(ctx.goat_client, graph, endorse_sigs).await + if let Err(e) = + todo_funcs::validate_finalized_graph(ctx.btc_client, ctx.goat_client, graph, endorse_sigs) + .await { if should_ignore_invalid_graph( &e, @@ -2546,7 +2591,9 @@ async fn handle_graph_finalize_default( ) -> Result<()> { // received from Operator // 1. check graph data - if let Err(e) = todo_funcs::validate_finalized_graph(ctx.goat_client, graph, endorse_sigs).await + if let Err(e) = + todo_funcs::validate_finalized_graph(ctx.btc_client, ctx.goat_client, graph, endorse_sigs) + .await { if should_ignore_invalid_graph( &e, @@ -4021,10 +4068,6 @@ async fn handle_assert_ready_operator( let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; - let mut assert_extra_data = - Vec::with_capacity(assert_witness.pi2.len() + assert_witness.pi3.len()); - assert_extra_data.extend_from_slice(&assert_witness.pi2); - assert_extra_data.extend_from_slice(&assert_witness.pi3); let mut asserted_operator_proof = Vec::new(); operator_proof.proof.serialize_compressed(&mut asserted_operator_proof)?; let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? @@ -4041,8 +4084,13 @@ async fn handle_assert_ready_operator( operator_state.asserted_operator_proof = Some(asserted_operator_proof); save_babe_setup_state(ctx.local_db, instance_id, graph_id, &setup_state)?; - let assert_tx = - operator_sign_assert(&mut graph, &assert_secret_key, &assert_message, &assert_extra_data)?; + let assert_tx = operator_sign_assert( + &mut graph, + &assert_secret_key, + &assert_message, + &assert_witness.pi2, + &assert_witness.pi3, + )?; let assert_tx_total_input_amount = graph.operator_assert.prev_outs().iter().map(|o| o.value).sum::(); broadcast_tx_with_cpfp(ctx.btc_client, assert_tx, assert_tx_total_input_amount).await?; @@ -5039,13 +5087,75 @@ async fn handle_sync_graph( ); return Ok(()); } + + if !ctx + .goat_client + .committee_mana_is_validate_peer_id(&ctx.from_peer_id.to_bytes()) + .await + .with_context(|| { + format!( + "failed to validate SyncGraph sender {} against the committee registry", + ctx.from_peer_id + ) + })? + { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: sender {} is not a registered committee peer", + ctx.from_peer_id + ); + return Ok(()); + } + + if graph.parameters.instance_parameters.instance_id != instance_id + || graph.parameters.graph_id != graph_id + { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: message identifiers do not match graph parameters" + ); + return Ok(()); + } + validate_graph_id_on_goat(ctx.goat_client, instance_id, graph_id).await.map_err(|e| { anyhow!( "Failed to validate graph_id on GoatChain for SyncGraph {instance_id}:{graph_id}: {e}" ) })?; - store_graph(ctx.local_db, graph).await?; + if let Err(e) = todo_funcs::validate_graph_instance_parameters( + ctx.btc_client, + ctx.goat_client, + &graph.parameters.instance_parameters, + ) + .await + { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: invalid instance parameters: {e}" + ); + return Ok(()); + } let graph = BitvmGcGraph::from_simplified(graph)?; + let graph_data = build_graph_data(&graph)?; + let graph_data_on_goat = ctx.goat_client.gateway_get_graph_data(&graph_id).await?; + if graph_data != graph_data_on_goat { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: reconstructed graph data does not match GoatChain" + ); + return Ok(()); + } + + if let Err(e) = verify_graph_operator_pre_signatures(&graph) { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: invalid operator pre-signatures: {e}" + ); + return Ok(()); + } + if let Err(e) = verify_graph_committee_pre_signatures(&graph) { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: invalid committee pre-signatures: {e}" + ); + return Ok(()); + } + let simplified_graph = graph.to_simplified()?; + store_graph(ctx.local_db, &simplified_graph).await?; refresh_and_compensate( ctx, instance_id, @@ -5112,6 +5222,7 @@ mod tests { OperatorBabeSetupState { frozen_verifier_pubkeys: Some(vec![verifier_pubkey()]), candidates: vec![OperatorVerifierCandidate { + verifier_peer_id: vec![1], verifier_pubkey: verifier_pubkey(), setup_package: package, verifier_index: Some(0), @@ -5129,6 +5240,7 @@ mod tests { let mut state = OperatorBabeSetupState { frozen_verifier_pubkeys: None, candidates: vec![OperatorVerifierCandidate { + verifier_peer_id: vec![1], verifier_pubkey: verifier_pubkey(), setup_package: package, verifier_index: None, diff --git a/node/src/utils.rs b/node/src/utils.rs index 2b1c30f4..52e4f640 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -206,45 +206,114 @@ pub mod todo_funcs { Ok(()) } - pub async fn validate_init_graph( - local_db: &LocalDB, + pub async fn validate_graph_instance_parameters( btc_client: &BTCClient, goat_client: &GOATClient, - graph: &SimplifiedBitvmGcGraph, + parameters: &BitvmGcInstanceParameters, ) -> Result<()> { - // Basic structural and on-chain consistency checks for an incoming graph proposal. - // Return SpecialError::InvalidGraph on any validation failure. - // 1) Rebuild full graph (ensures signatures present if flags are set and tx graph is coherent) - let full_graph = BitvmGcGraph::from_simplified(graph) - .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + let expected = super::read_instance_info_from_goat(goat_client, parameters.instance_id) + .await + .map_err(|e| { + SpecialError::InvalidGraph(format!( + "failed to load instance parameters from GoatChain: {e}" + )) + })?; + if parameters != &expected { + bail!(SpecialError::InvalidGraph( + "instance parameters mismatch with GoatChain peg-in data".to_string() + )); + } - // 2) Network must match local node network - let net = get_network(); - if graph.parameters.instance_parameters.network != net { + for input in &expected.user_info.inputs { + let funding_tx = btc_client.get_tx(&input.outpoint.txid).await.map_err(|e| { + SpecialError::InvalidGraph(format!( + "failed to load peg-in funding transaction {}: {e}", + input.outpoint.txid + )) + })?; + let Some(funding_tx) = funding_tx else { + bail!(SpecialError::InvalidGraph(format!( + "peg-in funding transaction {} not found on Bitcoin", + input.outpoint.txid + ))); + }; + let funding_output = + funding_tx.output.get(input.outpoint.vout as usize).ok_or_else(|| { + SpecialError::InvalidGraph(format!( + "peg-in funding outpoint {}:{} does not exist", + input.outpoint.txid, input.outpoint.vout + )) + })?; + if funding_output.value != input.amount { + bail!(SpecialError::InvalidGraph(format!( + "peg-in funding amount mismatch for {}:{}: graph={}, bitcoin={}", + input.outpoint.txid, + input.outpoint.vout, + input.amount.to_sat(), + funding_output.value.to_sat() + ))); + } + } + + let pegin_deposit_txid = expected + .build_pegin_tx() + .map_err(|e| { + SpecialError::InvalidGraph(format!("failed to reconstruct peg-in deposit: {e}")) + })? + .0 + .tx() + .compute_txid(); + if btc_client + .get_tx(&pegin_deposit_txid) + .await + .map_err(|e| { + SpecialError::InvalidGraph(format!( + "failed to load peg-in deposit transaction {pegin_deposit_txid}: {e}" + )) + })? + .is_none() + { bail!(SpecialError::InvalidGraph(format!( - "network mismatch: graph={:?} local={:?}", - graph.parameters.instance_parameters.network, net + "peg-in deposit transaction {pegin_deposit_txid} not found on Bitcoin" ))); } - // 3) Committee pubkeys must match what's registered on GoatChain for this instance - let instance_id = graph.parameters.instance_parameters.instance_id; - let committee_on_chain = - goat_client.gateway_get_committee_pubkeys(&instance_id).await.map_err(|e| { - SpecialError::InvalidGraph(format!("failed to load committee from chain: {e}")) - })?; - if committee_on_chain != graph.parameters.instance_parameters.committee_pubkeys { + Ok(()) + } + + pub async fn validate_init_graph( + btc_client: &BTCClient, + goat_client: &GOATClient, + graph: &SimplifiedBitvmGcGraph, + ) -> Result<()> { + if !graph.operator_pre_signed() { bail!(SpecialError::InvalidGraph( - "committee pubkeys mismatch with GoatChain".to_string() + "graph is missing operator pre-signatures".to_string() )); } + // Basic structural and on-chain consistency checks for an incoming graph proposal. + // Return SpecialError::InvalidGraph on any validation failure. + // 1) Rebuild full graph (ensures signatures present if flags are set and tx graph is coherent) + let full_graph = BitvmGcGraph::from_simplified(graph) + .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + verify_graph_operator_pre_signatures(&full_graph).map_err(|e| { + SpecialError::InvalidGraph(format!("invalid operator pre-signatures: {e}")) + })?; + + // 2) Bind all instance and peg-in parameters to GoatChain and Bitcoin. + validate_graph_instance_parameters( + btc_client, + goat_client, + &graph.parameters.instance_parameters, + ) + .await?; - // 4) Challenge amount and assert-commit count must match local constants + // 3) Challenge amount and assert-commit count must match local constants if graph.parameters.challenge_amount != super::todo_funcs::challenge_amount() { bail!(SpecialError::InvalidGraph("unexpected challenge amount".to_string())); } - // 5) Watchtower config sanity: number of watchtowers should match number of hashlocks and registry size + // 4) Watchtower config sanity: number of watchtowers should match number of hashlocks and registry size let watchtowers_on_chain = goat_client.committee_mana_get_watchtowers().await.map_err(|e| { SpecialError::InvalidGraph(format!("failed to load watchtowers from chain: {e}")) @@ -258,7 +327,7 @@ pub mod todo_funcs { graph.parameters.pubin_disprove_constant, )?; - // 6) Operator stake sanity: verify operator is registered and has enough locked stake + // 5) Operator stake sanity: verify operator is registered and has enough locked stake let op_pk_bytes = graph.parameters.operator_pubkey.to_bytes(); let xonly: [u8; 32] = op_pk_bytes[1..33] .try_into() @@ -286,32 +355,35 @@ pub mod todo_funcs { Ok(()) } pub async fn validate_finalized_graph( + btc_client: &BTCClient, goat_client: &GOATClient, graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, EvmAddress, Vec)], ) -> Result<()> { + if !graph.operator_pre_signed() || !graph.committee_pre_signed() { + bail!(SpecialError::InvalidGraph( + "finalized graph is missing operator or committee pre-signatures".to_string() + )); + } // 1) Rebuild full graph to ensure structure is coherent and txns derivable let full_graph = BitvmGcGraph::from_simplified(graph) .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + verify_graph_operator_pre_signatures(&full_graph).map_err(|e| { + SpecialError::InvalidGraph(format!("invalid operator pre-signatures: {e}")) + })?; + verify_graph_committee_pre_signatures(&full_graph).map_err(|e| { + SpecialError::InvalidGraph(format!("invalid committee pre-signatures: {e}")) + })?; + + // 2) Repeat the full instance and peg-in binding for nodes that did not see CreateGraph. + validate_graph_instance_parameters( + btc_client, + goat_client, + &graph.parameters.instance_parameters, + ) + .await?; - // 2) Repeat key static checks (network, committee set, counts) - let net = get_network(); - if graph.parameters.instance_parameters.network != net { - bail!(SpecialError::InvalidGraph(format!( - "network mismatch: graph={:?} local={:?}", - graph.parameters.instance_parameters.network, net - ))); - } let instance_id = graph.parameters.instance_parameters.instance_id; - let committee_on_chain = - goat_client.gateway_get_committee_pubkeys(&instance_id).await.map_err(|e| { - SpecialError::InvalidGraph(format!("failed to load committee from chain: {e}")) - })?; - if committee_on_chain != graph.parameters.instance_parameters.committee_pubkeys { - bail!(SpecialError::InvalidGraph( - "committee pubkeys mismatch with GoatChain".to_string() - )); - } if graph.parameters.challenge_amount != super::todo_funcs::challenge_amount() { bail!(SpecialError::InvalidGraph("unexpected challenge amount".to_string())); } @@ -335,6 +407,18 @@ pub mod todo_funcs { let pegin_data = goat_client.gateway_get_pegin_data(&instance_id).await.map_err(|e| { SpecialError::InvalidGraph(format!("failed to load instance data: {e}")) })?; + if pegin_data.committee_pubkeys.len() != pegin_data.committee_addresses.len() { + bail!(SpecialError::InvalidGraph( + "on-chain committee pubkey and address counts differ".to_string() + )); + } + if endorse_sigs.len() != pegin_data.committee_pubkeys.len() { + bail!(SpecialError::InvalidGraph(format!( + "endorsement count {} does not match instance committee count {}", + endorse_sigs.len(), + pegin_data.committee_pubkeys.len() + ))); + } for (pk, evm_addr, sig) in endorse_sigs.iter() { // no duplicates @@ -5107,6 +5191,7 @@ pub struct VerifierBabeSetupState { #[derive(Clone, Serialize, Deserialize)] pub struct OperatorVerifierCandidate { + pub verifier_peer_id: Vec, pub verifier_pubkey: PublicKey, pub setup_package: CACSetupPackage, pub verifier_index: Option, From 2c5a426479c757beaef1dea31475f3177c1e9f3f Mon Sep 17 00:00:00 2001 From: Blake Hau <0xblake.sg@gmail.com> Date: Thu, 2 Jul 2026 15:29:07 +0800 Subject: [PATCH 10/14] Fix/gc v2 debug (#412) * Increase max transmit size to 16 MB in gossipsub configuration * Update NODE_TESTNET_TIMELOCK_CONFIG values * Refactor BABE setup state management and soldering proof handling * Add ENABLE_BABE_SETUP_STATE_CLEANUP feature flag and monitor logic * Validate commitment count in GenCircuits setup package --- .gitignore | 4 +- node/README.md | 1 + node/src/action.rs | 2 +- node/src/env.rs | 8 + node/src/handle.rs | 263 +++++++++++------- node/src/middleware/behaviour.rs | 2 +- .../babe_setup_state_cleanup_task.rs | 13 + node/src/scheduled_tasks/mod.rs | 14 +- node/src/soldering_payload_store.rs | 13 + node/src/utils.rs | 139 +++++++-- 10 files changed, 319 insertions(+), 140 deletions(-) create mode 100644 node/src/scheduled_tasks/babe_setup_state_cleanup_task.rs diff --git a/.gitignore b/.gitignore index 6d9263a0..13c8f4ab 100644 --- a/.gitignore +++ b/.gitignore @@ -34,6 +34,4 @@ circuits/*/*/*.bin.in **/*.out **/*/output.data* -proof-builder-rpc/*.ckpt - -runner \ No newline at end of file +proof-builder-rpc/*.ckpt \ No newline at end of file diff --git a/node/README.md b/node/README.md index 9e65ac18..8bf705ea 100644 --- a/node/README.md +++ b/node/README.md @@ -872,6 +872,7 @@ real graph raw data in the database. | `GOAT_PRIVATE_KEY` | Conditional | GOAT chain private key (required for Committee) | - | | `GOAT_ADDRESS` | Conditional | GOAT address (required for Operator/Verifier) | - | | `ENABLE_RELAYER` | No | Enable relayer mode for Committee nodes | `false` | +| `ENABLE_BABE_SETUP_STATE_CLEANUP` | No | Enable scheduled BABE setup state cleanup for Operator/Verifier nodes | `false` | | `BTC_CHAIN_URL` | No | Bitcoin Esplora API endpoint | Public Esplora | | `MARA_SLIPSTREAM_API_URL` | No | MARA slipstream API base URL (used for non-standard tx broadcast) | mainnet: `https://slipstream.mara.com/api`; testnet4: `https://teststream.mara.com/api` | | `GOAT_PROOF_BUILD_URL` | No | Proof Builder RPC endpoint | - | diff --git a/node/src/action.rs b/node/src/action.rs index b350f87a..96095162 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -112,7 +112,7 @@ pub struct CutCircuits { pub verifier_index: usize, pub selected_circuit_indexes: Vec, } -#[derive(Serialize, Deserialize, Clone)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct SolderingProofReady { pub instance_id: Uuid, pub graph_id: Uuid, diff --git a/node/src/env.rs b/node/src/env.rs index 355a3c46..072c7161 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -34,6 +34,7 @@ pub const ENV_GOAT_SEQUENCER_SET_MULTI_SIG_VERIFIER_ADDRESS: &str = "ENV_GOAT_SEQUENCER_SET_MULTI_SIG_VERIFIER_ADDRESS"; pub const ENV_ENABLE_RELAYER: &str = "ENABLE_RELAYER"; pub const ENV_ENABLE_UPDATE_SPV_CONTRACT: &str = "ENABLE_UPDATE_SPV_CONTRACT"; +pub const ENV_ENABLE_BABE_SETUP_STATE_CLEANUP: &str = "ENABLE_BABE_SETUP_STATE_CLEANUP"; pub const ENV_BTC_BLOCK_CONFIRMS: &str = "BTC_BLOCK_CONFIRMS"; pub const ENV_MARA_SLIPSTREAM_API_URL: &str = "MARA_SLIPSTREAM_API_URL"; pub const DEFAULT_MARA_SLIPSTREAM_MAINNET_API_URL: &str = "https://slipstream.mara.com/api"; @@ -241,6 +242,13 @@ pub fn is_enable_update_spv_contract() -> bool { } } +pub fn is_enable_babe_setup_state_cleanup() -> bool { + match std::env::var(ENV_ENABLE_BABE_SETUP_STATE_CLEANUP) { + Ok(value) => value.eq_ignore_ascii_case("true"), + Err(_) => false, + } +} + pub fn get_btc_block_confirms(network: Network) -> u32 { std::env::var(ENV_BTC_BLOCK_CONFIRMS) .ok() diff --git a/node/src/handle.rs b/node/src/handle.rs index 3802f977..3101590d 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -17,12 +17,12 @@ use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{ BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, - CACSetupPackage, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, TxAssertWitness, - WOTS_SIG_COUNT, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, - build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, - extract_gc_circuit_data, open_real_setup_and_solder, - recover_operator_proof_from_assert_witness, recover_real_wrongly_challenged_witness, - verify_real_setup, + CACSetupPackage, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, + FinalizedInstanceData, SolderingData, TxAssertWitness, WOTS_SIG_COUNT, assert_wots_message, + build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, + derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, + open_real_setup_and_solder, recover_operator_proof_from_assert_witness, + recover_real_wrongly_challenged_witness, verify_real_setup, }; use bitvm_lib::committee::*; use bitvm_lib::keys::*; @@ -647,7 +647,8 @@ fn record_candidate_gc_data( verifier_index: usize, setup_package: &CACSetupPackage, gc_data: BitvmGcCircuitData, - prover_state: BabeProverState, + prover_state: &BabeProverState, + soldering_proof_ready: SolderingProofReady, ) -> Result>> { let frozen = state .frozen_verifier_pubkeys @@ -679,6 +680,9 @@ fn record_candidate_gc_data( if candidate.selected_circuit_indexes != prover_state.soldering.finalized_indices { bail!("BABE prover state finalized indices do not match selected verifier cut"); } + if soldering_proof_ready.verifier_index != verifier_index { + bail!("soldering proof reference verifier index does not match selected verifier slot"); + } if prover_state.finalized.len() != BABE_M_CC || prover_state.h_msgs.len() != BABE_M_CC { bail!("BABE prover state must contain exactly {BABE_M_CC} finalized instances and hashes"); } @@ -690,16 +694,16 @@ fn record_candidate_gc_data( { bail!("conflicting GC slot received for selected verifier"); } - if let Some(existing) = &candidate.prover_state - && existing != &prover_state + if let Some(existing) = &candidate.soldering_proof_ready + && existing != &soldering_proof_ready { - bail!("conflicting BABE prover state received for selected verifier"); + bail!("conflicting soldering proof reference received for selected verifier"); } if candidate.gc_data.is_none() { candidate.gc_data = Some(gc_data); } - if candidate.prover_state.is_none() { - candidate.prover_state = Some(prover_state); + if candidate.soldering_proof_ready.is_none() { + candidate.soldering_proof_ready = Some(soldering_proof_ready); } if state.candidates.iter().any(|candidate| candidate.gc_data.is_none()) { return Ok(None); @@ -707,6 +711,24 @@ fn record_candidate_gc_data( Ok(Some(state.candidates.iter().map(|candidate| candidate.gc_data.clone().unwrap()).collect())) } +fn build_babe_prover_state( + setup_package: &CACSetupPackage, + finalized: Vec, + soldering: SolderingData, +) -> Result { + let h_msgs = finalized + .iter() + .map(|finalized| { + setup_package + .commits + .get(finalized.index) + .map(|commit| commit.h_msg) + .ok_or_else(|| anyhow!("finalized BABE instance index is out of range")) + }) + .collect::>>()?; + Ok(BabeProverState { package: setup_package.clone(), finalized, soldering, h_msgs }) +} + fn validate_verifier_slot_lengths( verifier_index: usize, gc_data_len: usize, @@ -1219,11 +1241,8 @@ async fn handle_init_graph_verifier( verifier_pubkey, setup_package, private_state, - verifier_index: None, finalized_indices: vec![], - opened: vec![], - finalized: vec![], - soldering: None, + soldering_proof_ready: None, } }; @@ -1268,8 +1287,11 @@ async fn handle_gen_circuits_operator( return Ok(()); } - if setup_package.commits.is_empty() { - bail!("GenCircuits setup package has no commitments"); + if setup_package.commits.len() != BABE_N_CC { + bail!( + "invalid GenCircuits setup package commitment count: expected {BABE_N_CC}, got {}", + setup_package.commits.len() + ); } let verifier_peer_id = ctx.from_peer_id.to_bytes(); @@ -1332,7 +1354,7 @@ async fn handle_gen_circuits_operator( verifier_index: None, selected_circuit_indexes: vec![], gc_data: None, - prover_state: None, + soldering_proof_ready: None, }); } @@ -1408,30 +1430,26 @@ async fn handle_cut_circuits_verifier( return Ok(()); } - if let Some(saved_index) = verifier_state.verifier_index { - if saved_index != verifier_index { + if let Some(soldering_proof_ready) = verifier_state.soldering_proof_ready.clone() { + if soldering_proof_ready.verifier_index != verifier_index { bail!( - "CutCircuits verifier index {verifier_index} conflicts with persisted slot {saved_index}" + "CutCircuits verifier index {verifier_index} conflicts with persisted slot {}", + soldering_proof_ready.verifier_index ); } if verifier_state.finalized_indices != *selected_circuit_indexes { bail!("CutCircuits finalized indices conflict with persisted selection"); } - if let Some(soldering) = verifier_state.soldering.clone() { - let setup_state = verifier_state; - send_soldering_proof_to_operator( - ctx.swarm, - instance_id, - graph_id, - verifier_index, - &setup_state.opened, - &setup_state.finalized, - &soldering, - ) - .await?; + send_to_peer( + ctx.swarm, + GOATMessage::new( + Actor::Operator, + GOATMessageContent::SolderingProofReady(soldering_proof_ready), + ), + ) + .await?; - return Ok(()); - } + return Ok(()); } let setup_package = verifier_state.setup_package.clone(); @@ -1460,24 +1478,28 @@ async fn handle_cut_circuits_verifier( .await .context("real BABE opening task failed")??; - verifier_state.verifier_index = Some(verifier_index); + let soldering_proof_ready = save_soldering_proof_payload( + instance_id, + graph_id, + verifier_index, + &opened, + &finalized, + &soldering, + ) + .await?; verifier_state.finalized_indices = selected_circuit_indexes.clone(); - verifier_state.opened = opened.clone(); - verifier_state.finalized = finalized.clone(); - verifier_state.soldering = Some(soldering.clone()); + verifier_state.soldering_proof_ready = Some(soldering_proof_ready.clone()); update_babe_setup_state(ctx.local_db, instance_id, graph_id, |state| { state.verifier = Some(verifier_state); })?; - send_soldering_proof_to_operator( + send_to_peer( ctx.swarm, - instance_id, - graph_id, - verifier_index, - &opened, - &finalized, - &soldering, + GOATMessage::new( + Actor::Operator, + GOATMessageContent::SolderingProofReady(soldering_proof_ready), + ), ) .await?; @@ -1496,6 +1518,8 @@ async fn handle_soldering_proof_ready_operator( if total_len == 0 { bail!("SolderingProofReady total_len must be greater than zero"); } + let soldering_proof_ready = + SolderingProofReady { instance_id, graph_id, verifier_index, payload_hash, total_len }; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let local_operator_pubkey = operator_master_key.master_keypair().public_key().into(); if !pending_graph_belongs_to_operator( @@ -1593,29 +1617,16 @@ async fn handle_soldering_proof_ready_operator( payload_hash = %soldering_payload_hash_hex(&payload_hash), "start processing soldering proof payload" ); - handle_soldering_proof_payload_operator( - ctx, - instance_id, - graph_id, - verifier_index, - payload_hash, - total_len, - &payload, - ) - .await + handle_soldering_proof_payload_operator(ctx, &soldering_proof_ready, &payload).await } -#[allow(clippy::too_many_arguments)] -pub(crate) async fn handle_soldering_proof_payload_operator( - ctx: &mut HandlerContext<'_>, - instance_id: Uuid, - graph_id: Uuid, - verifier_index: usize, - payload_hash: [u8; 32], - total_len: usize, +fn decode_soldering_proof_payload( + soldering_proof_ready: &SolderingProofReady, payload: &[u8], -) -> Result<()> { - if payload.len() != total_len { +) -> Result { + let SolderingProofReady { instance_id, graph_id, verifier_index, payload_hash, total_len } = + soldering_proof_ready; + if payload.len() != *total_len { tracing::warn!( instance_id = %instance_id, graph_id = %graph_id, @@ -1631,7 +1642,7 @@ pub(crate) async fn handle_soldering_proof_payload_operator( ); } let actual_hash = soldering_payload_hash(payload); - if actual_hash != payload_hash { + if actual_hash != *payload_hash { tracing::warn!( instance_id = %instance_id, graph_id = %graph_id, @@ -1642,21 +1653,28 @@ pub(crate) async fn handle_soldering_proof_payload_operator( ); bail!("SolderingProof payload hash mismatch"); } - let payload: CompactSolderingProofPayload = - bincode::deserialize(payload).context("deserialize compact soldering proof payload")?; - handle_compact_soldering_proof_operator(ctx, instance_id, graph_id, verifier_index, payload) - .await + bincode::deserialize(payload).context("deserialize compact soldering proof payload") +} + +pub(crate) async fn handle_soldering_proof_payload_operator( + ctx: &mut HandlerContext<'_>, + soldering_proof_ready: &SolderingProofReady, + payload: &[u8], +) -> Result<()> { + let payload = decode_soldering_proof_payload(soldering_proof_ready, payload)?; + handle_compact_soldering_proof_operator(ctx, soldering_proof_ready, payload).await } // verify Verifier SolderingProof, build Graph and broadcast CreateGraph. -#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %soldering_proof_ready.instance_id, graph_id = %soldering_proof_ready.graph_id))] async fn handle_compact_soldering_proof_operator( ctx: &mut HandlerContext<'_>, - instance_id: Uuid, - graph_id: Uuid, - verifier_index: usize, + soldering_proof_ready: &SolderingProofReady, payload: CompactSolderingProofPayload, ) -> Result<()> { + let instance_id = soldering_proof_ready.instance_id; + let graph_id = soldering_proof_ready.graph_id; + let verifier_index = soldering_proof_ready.verifier_index; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let local_operator_pubkey = operator_master_key.master_keypair().public_key().into(); if !pending_graph_belongs_to_operator( @@ -1730,22 +1748,16 @@ async fn handle_compact_soldering_proof_operator( bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); } let epk = &setup_package.commits[finalized[0].index].epk; - let h_msgs: Vec<[u8; 20]> = - finalized.iter().map(|f| setup_package.commits[f.index].h_msg).collect(); - let gc_data = extract_gc_circuit_data(verifier_pubkey, epk, &h_msgs)?; - let prover_state = BabeProverState { - package: setup_package.clone(), - finalized, - soldering, - h_msgs: gc_data.final_msg_hashlocks.clone(), - }; + let prover_state = build_babe_prover_state(&setup_package, finalized, soldering)?; + let gc_data = extract_gc_circuit_data(verifier_pubkey, epk, &prover_state.h_msgs)?; let Some(bitvm_gc_circuit_datas) = record_candidate_gc_data( operator_state, verifier_pubkey, verifier_index, &setup_package, gc_data, - prover_state, + &prover_state, + soldering_proof_ready.clone(), )? else { save_babe_setup_state(ctx.local_db, instance_id, graph_id, &state)?; @@ -4315,7 +4327,13 @@ async fn handle_assert_sent_verifier( ); return Ok(()); }; - if saved_verifier_state.verifier_index != Some(verifier_index) { + let Some(soldering_proof_ready) = saved_verifier_state.soldering_proof_ready.as_ref() else { + tracing::warn!( + "Ignore AssertSent for {instance_id}:{graph_id}: missing soldering proof reference" + ); + return Ok(()); + }; + if soldering_proof_ready.verifier_index != verifier_index { tracing::warn!( "Ignore AssertSent for {instance_id}:{graph_id}: local setup slot does not match graph owner slot" ); @@ -4445,10 +4463,24 @@ async fn handle_challenge_assert_sent_operator( .iter() .find(|candidate| candidate.verifier_index == Some(verifier_index)) .ok_or_else(|| anyhow!("missing BABE prover state for verifier slot {verifier_index}"))?; - let prover_state = candidate - .prover_state - .as_ref() - .ok_or_else(|| anyhow!("missing BABE prover state for verifier slot {verifier_index}"))?; + let soldering_proof_ready = candidate.soldering_proof_ready.as_ref().ok_or_else(|| { + anyhow!("missing soldering proof reference for verifier slot {verifier_index}") + })?; + let store_base_path = get_soldering_proof_payload_store_path()?; + let payload_path = soldering_proof_payload_store_path( + &store_base_path, + instance_id, + graph_id, + verifier_index, + &soldering_proof_ready.payload_hash, + )?; + let payload = read_soldering_proof_store_payload(&payload_path) + .await + .with_context(|| format!("read soldering proof payload from {payload_path}"))?; + let payload = decode_soldering_proof_payload(soldering_proof_ready, &payload)?; + let (_, finalized, soldering) = expand_compact_soldering_proof_payload(payload) + .context("expand compact soldering proof payload for challenge")?; + let prover_state = build_babe_prover_state(&candidate.setup_package, finalized, soldering)?; let assert_witness = TxAssertWitness { wots_sig: challenge_witness.witness.wots_sig.clone(), pi2: operator_assert_witness.pi2, @@ -4463,7 +4495,7 @@ async fn handle_challenge_assert_sent_operator( anyhow!("cannot recover dynamic input from challenge witness WOTS signature") })?; let wrongly_challenged_witness = recover_real_wrongly_challenged_witness( - prover_state, + &prover_state, &challenge_witness, &proof, vk, @@ -5218,6 +5250,16 @@ mod tests { (package, gc_data, prover_state) } + fn soldering_proof_ready(hash_byte: u8) -> SolderingProofReady { + SolderingProofReady { + instance_id: Uuid::nil(), + graph_id: Uuid::nil(), + verifier_index: 0, + payload_hash: [hash_byte; 32], + total_len: 1, + } + } + fn operator_state(package: CACSetupPackage) -> OperatorBabeSetupState { OperatorBabeSetupState { frozen_verifier_pubkeys: Some(vec![verifier_pubkey()]), @@ -5228,7 +5270,7 @@ mod tests { verifier_index: Some(0), selected_circuit_indexes: (0..BABE_M_CC).collect(), gc_data: None, - prover_state: None, + soldering_proof_ready: None, }], asserted_operator_proof: None, } @@ -5246,7 +5288,7 @@ mod tests { verifier_index: None, selected_circuit_indexes: vec![], gc_data: None, - prover_state: None, + soldering_proof_ready: None, }], asserted_operator_proof: None, }; @@ -5257,9 +5299,10 @@ mod tests { } #[test] - fn candidate_records_one_slot_and_full_prover_state_idempotently() { + fn candidate_records_one_slot_and_payload_reference_idempotently() { let (package, gc_data, prover_state) = gc_submission(); let mut state = operator_state(package.clone()); + let soldering_proof_ready = soldering_proof_ready(1); let graph_data = record_candidate_gc_data( &mut state, @@ -5267,14 +5310,18 @@ mod tests { 0, &package, gc_data.clone(), - prover_state.clone(), + &prover_state, + soldering_proof_ready.clone(), ) .unwrap() .unwrap(); assert_eq!(graph_data.len(), 1); assert_eq!(graph_data[0].final_msg_hashlocks.len(), BABE_M_CC); - assert_eq!(state.candidates[0].prover_state.as_ref().unwrap().finalized.len(), BABE_M_CC); + assert_eq!( + state.candidates[0].soldering_proof_ready.as_ref(), + Some(&soldering_proof_ready) + ); let duplicate = record_candidate_gc_data( &mut state, @@ -5282,7 +5329,8 @@ mod tests { 0, &package, gc_data.clone(), - prover_state.clone(), + &prover_state, + soldering_proof_ready.clone(), ) .unwrap() .unwrap(); @@ -5297,21 +5345,26 @@ mod tests { 0, &package, gc_data, - conflict, + &conflict, + soldering_proof_ready, ) .is_err() ); } #[test] - fn legacy_operator_candidate_without_prover_state_deserializes() { - let (package, _, _) = gc_submission(); - let mut value = serde_json::to_value(operator_state(package)).unwrap(); - value["candidates"][0].as_object_mut().unwrap().remove("prover_state"); + fn rebuilds_prover_state_from_persisted_payload_data() { + let package = build_setup_package(BABE_M_CC + 1).unwrap(); + let selected = (0..BABE_M_CC).collect::>(); + let (_, finalized, soldering) = open_and_solder(&package, &selected).unwrap(); - let restored: OperatorBabeSetupState = serde_json::from_value(value).unwrap(); + let restored = + build_babe_prover_state(&package, finalized.clone(), soldering.clone()).unwrap(); - assert!(restored.candidates[0].prover_state.is_none()); + assert_eq!(restored.package, package); + assert_eq!(restored.finalized, finalized); + assert_eq!(restored.soldering, soldering); + assert_eq!(restored.h_msgs.len(), BABE_M_CC); } // ── collect_ack_txins ───────────────────────────────────────────────────── diff --git a/node/src/middleware/behaviour.rs b/node/src/middleware/behaviour.rs index f7773fe7..378d5371 100644 --- a/node/src/middleware/behaviour.rs +++ b/node/src/middleware/behaviour.rs @@ -21,7 +21,7 @@ impl AllBehaviours { // .unwrap(); let gossipsub_config = gossipsub::ConfigBuilder::default() - .max_transmit_size(4194304) // 4 MB + .max_transmit_size(16 * 1024 * 1024) // 16 MB .build() .map_err(io::Error::other) .unwrap(); diff --git a/node/src/scheduled_tasks/babe_setup_state_cleanup_task.rs b/node/src/scheduled_tasks/babe_setup_state_cleanup_task.rs new file mode 100644 index 00000000..086fca53 --- /dev/null +++ b/node/src/scheduled_tasks/babe_setup_state_cleanup_task.rs @@ -0,0 +1,13 @@ +use crate::env::get_soldering_proof_payload_store_path; +use crate::utils::cleanup_babe_setup_states; +use store::localdb::LocalDB; +use tracing::info; + +pub async fn babe_setup_state_cleanup_monitor(local_db: &LocalDB) -> anyhow::Result<()> { + let payload_store = get_soldering_proof_payload_store_path()?; + let deleted = cleanup_babe_setup_states(local_db, &payload_store).await?; + if deleted > 0 { + info!(deleted, "BABE setup state cleanup monitor finished"); + } + Ok(()) +} diff --git a/node/src/scheduled_tasks/mod.rs b/node/src/scheduled_tasks/mod.rs index 7544cb99..e590752e 100644 --- a/node/src/scheduled_tasks/mod.rs +++ b/node/src/scheduled_tasks/mod.rs @@ -1,3 +1,4 @@ +mod babe_setup_state_cleanup_task; mod event_watch_task; pub mod graph_maintenance_tasks; pub mod instance_maintenance_tasks; @@ -6,7 +7,11 @@ mod sequencer_set_hash_monitor_task; mod spv_maintenance_tasks; use crate::action::GOATMessageContent; -use crate::env::{get_maintenance_run_timeout_secs, is_enable_update_spv_contract, is_relayer}; +use crate::env::{ + get_maintenance_run_timeout_secs, is_enable_babe_setup_state_cleanup, + is_enable_update_spv_contract, is_relayer, +}; +use crate::scheduled_tasks::babe_setup_state_cleanup_task::babe_setup_state_cleanup_monitor; use crate::scheduled_tasks::graph_maintenance_tasks::{ detect_init_withdraw_call, detect_kickoff, detect_take1_or_challenge, process_graph_challenge, }; @@ -55,6 +60,13 @@ async fn run( let btc_client = btc_client.as_ref(); let goat_client = goat_client.as_ref(); + if is_enable_babe_setup_state_cleanup() + && matches!(&actor, Actor::Verifier | Actor::Operator | Actor::All) + && let Err(err) = babe_setup_state_cleanup_monitor(local_db).await + { + warn!("babe_setup_state_cleanup_monitor, err {:?}", err) + } + if (actor == Actor::Operator || is_relayer()) && let Err(err) = node_available_pbtc_update_monitor(local_db, goat_client).await { diff --git a/node/src/soldering_payload_store.rs b/node/src/soldering_payload_store.rs index fae3edc6..aa683546 100644 --- a/node/src/soldering_payload_store.rs +++ b/node/src/soldering_payload_store.rs @@ -109,6 +109,19 @@ pub(crate) async fn read_soldering_proof_store_payload(path: &str) -> Result Result<()> { + if is_soldering_proof_s3_path(path) { + bail!("S3 soldering proof cleanup is not supported"); + } + match tokio::fs::remove_file(path.trim()).await { + Ok(()) => Ok(()), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()), + Err(err) => { + Err(err).with_context(|| format!("delete soldering proof payload {}", path.trim())) + } + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/node/src/utils.rs b/node/src/utils.rs index 52e4f640..3823fcbb 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -8,8 +8,8 @@ use crate::error::SpecialError; use crate::middleware::AllBehaviours; use crate::rpc_service::current_time_secs; use crate::soldering_payload_store::{ - is_soldering_proof_s3_path, soldering_proof_payload_store_path, - write_soldering_proof_store_payload, + delete_soldering_proof_store_payload, is_soldering_proof_s3_path, + soldering_proof_payload_store_path, write_soldering_proof_store_payload, }; use alloy::primitives::{Address as EvmAddress, Signature as EvmSignature}; use alloy::signers::Signer; @@ -85,8 +85,8 @@ use crate::scheduled_tasks::graph_maintenance_tasks::{ }; use bitcoin_light_client_circuit::hash_operator_constant; use bitvm_lib::babe_adapter::{ - BabeProverState, BabeVerifierPrivateState, CACSetupPackage, FinalizedInstanceData, - SolderingData, compact_soldering_proof_payload, + BabeVerifierPrivateState, CACSetupPackage, FinalizedInstanceData, SolderingData, + compact_soldering_proof_payload, }; use bitvm_lib::transactions::base::BaseTransaction; use client::goat_chain::{DisproveTxType, GraphData, PeginStatus, WithdrawStatus}; @@ -5043,7 +5043,7 @@ pub(crate) fn babe_setup_state_root(local_db: &LocalDB) -> PathBuf { } fn babe_setup_state_path(local_db: &LocalDB, instance_id: Uuid, graph_id: Uuid) -> PathBuf { - babe_setup_state_root(local_db).join(instance_id.to_string()).join(format!("{graph_id}.json")) + babe_setup_state_root(local_db).join(instance_id.to_string()).join(format!("{graph_id}.bin")) } pub(crate) fn soldering_payload_hash(payload: &[u8]) -> [u8; 32] { @@ -5067,15 +5067,14 @@ pub(crate) async fn pending_graph_belongs_to_operator( } #[allow(clippy::too_many_arguments)] -pub(crate) async fn send_soldering_proof_to_operator( - swarm: &mut Swarm, +pub(crate) async fn save_soldering_proof_payload( instance_id: Uuid, graph_id: Uuid, verifier_index: usize, opened: &[(usize, u64)], finalized: &[FinalizedInstanceData], soldering: &SolderingData, -) -> Result<()> { +) -> Result { let compact_payload = compact_soldering_proof_payload(opened, finalized, soldering)?; let payload = bincode::serialize(&compact_payload) .context("serialize compact soldering proof payload")?; @@ -5110,23 +5109,17 @@ pub(crate) async fn send_soldering_proof_to_operator( total_len, payload_hash = %soldering_payload_hash_hex(&payload_hash), payload_path = %payload_path, - "send compact soldering proof ready from payload store" + "saved compact soldering proof to payload store" ); - let message_content = GOATMessageContent::SolderingProofReady(SolderingProofReady { - instance_id, - graph_id, - verifier_index, - payload_hash, - total_len, - }); - send_to_peer(swarm, GOATMessage::new(Actor::Operator, message_content)).await?; - - Ok(()) + Ok(SolderingProofReady { instance_id, graph_id, verifier_index, payload_hash, total_len }) } fn load_babe_setup_state_from_path(path: &Path) -> Result> { match std::fs::read(path) { - Ok(bytes) => Ok(Some(serde_json::from_slice(&bytes)?)), + Ok(bytes) => Ok(Some( + bincode::deserialize(&bytes) + .with_context(|| format!("decode BABE setup state {}", path.display()))?, + )), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(err).with_context(|| format!("read BABE setup state {}", path.display())), } @@ -5137,9 +5130,15 @@ fn save_babe_setup_state_to_path(path: &Path, state: &BabeSetupState) -> Result< std::fs::create_dir_all(parent) .with_context(|| format!("create BABE setup state dir {}", parent.display()))?; } - let bytes = serde_json::to_vec_pretty(state)?; - std::fs::write(path, bytes) - .with_context(|| format!("write BABE setup state {}", path.display())) + let bytes = bincode::serialize(state)?; + let mut temp_path = path.as_os_str().to_os_string(); + temp_path.push(".tmp"); + let temp_path = PathBuf::from(temp_path); + std::fs::write(&temp_path, bytes) + .with_context(|| format!("write BABE setup state {}", temp_path.display()))?; + std::fs::rename(&temp_path, path).with_context(|| { + format!("move BABE setup state {} to {}", temp_path.display(), path.display()) + }) } pub(crate) fn load_babe_setup_state( @@ -5171,6 +5170,92 @@ pub(crate) fn update_babe_setup_state( Ok(state) } +pub(crate) async fn cleanup_babe_setup_states( + local_db: &LocalDB, + payload_store: &str, +) -> Result { + let root = babe_setup_state_root(local_db); + let instance_dirs = match std::fs::read_dir(&root) { + Ok(entries) => entries, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => return Ok(0), + Err(err) => { + return Err(err) + .with_context(|| format!("read BABE setup state root {}", root.display())); + } + }; + let mut storage = local_db.acquire().await?; + let mut deleted = 0; + for instance_dir in instance_dirs { + let instance_dir = instance_dir?; + if !instance_dir.file_type()?.is_dir() { + continue; + } + let Ok(instance_id) = Uuid::parse_str(&instance_dir.file_name().to_string_lossy()) else { + continue; + }; + for state_file in std::fs::read_dir(instance_dir.path())? { + let state_file = state_file?; + let path = state_file.path(); + if path.extension().and_then(|extension| extension.to_str()) != Some("bin") { + continue; + } + let Some(graph_id) = path + .file_stem() + .and_then(|stem| stem.to_str()) + .and_then(|stem| Uuid::parse_str(stem).ok()) + else { + continue; + }; + let Some(graph) = storage.find_graph(&graph_id).await? else { + continue; + }; + let Ok(status) = GraphStatus::from_str(&graph.status) else { + warn!( + "Skip BABE setup cleanup for {graph_id}: invalid graph status {}", + graph.status + ); + continue; + }; + if graph.instance_id != instance_id || !status.is_closed() { + continue; + } + let Some(state) = load_babe_setup_state_from_path(&path)? else { + continue; + }; + let mut payloads = Vec::new(); + if let Some(ready) = state.verifier.and_then(|verifier| verifier.soldering_proof_ready) + { + payloads.push(ready); + } + if let Some(operator) = state.operator { + payloads.extend( + operator + .candidates + .into_iter() + .filter_map(|candidate| candidate.soldering_proof_ready), + ); + } + for ready in payloads { + if ready.instance_id != instance_id || ready.graph_id != graph_id { + bail!("BABE setup payload reference does not match state path"); + } + let payload_path = soldering_proof_payload_store_path( + payload_store, + instance_id, + graph_id, + ready.verifier_index, + &ready.payload_hash, + )?; + delete_soldering_proof_store_payload(&payload_path).await?; + } + std::fs::remove_file(&path) + .with_context(|| format!("delete BABE setup state {}", path.display()))?; + deleted += 1; + } + } + Ok(deleted) +} + #[derive(Clone, Default, Serialize, Deserialize)] pub struct BabeSetupState { pub verifier: Option, @@ -5182,11 +5267,8 @@ pub struct VerifierBabeSetupState { pub verifier_pubkey: PublicKey, pub setup_package: CACSetupPackage, pub private_state: BabeVerifierPrivateState, - pub verifier_index: Option, pub finalized_indices: Vec, - pub opened: Vec<(usize, u64)>, - pub finalized: Vec, - pub soldering: Option, + pub soldering_proof_ready: Option, } #[derive(Clone, Serialize, Deserialize)] @@ -5197,8 +5279,7 @@ pub struct OperatorVerifierCandidate { pub verifier_index: Option, pub selected_circuit_indexes: Vec, pub gc_data: Option, - #[serde(default)] - pub prover_state: Option, + pub soldering_proof_ready: Option, } #[derive(Clone, Serialize, Deserialize)] From d6e34dfd25727cb7d8e826576f1d21b397dcd285 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ph=E1=BA=A1m=20Gia=20Vi=E1=BB=87t=20Anh?= Date: Fri, 3 Jul 2026 20:09:42 +0700 Subject: [PATCH 11/14] adapt to lastest change of bitvm-gc (#413) --- Cargo.lock | 6 +++--- crates/bitvm-gc/src/babe_adapter.rs | 9 ++++----- crates/bitvm-gc/tests/babe_adapter.rs | 1 + node/src/handle.rs | 2 ++ 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 93de5988..df4b113a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5577,7 +5577,7 @@ dependencies = [ [[package]] name = "garbled-snark-verifier" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#d1a69cf194e04fc413c6cdd1e8d47b4e834c4ddc" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#f1999e03899ae43d46f191d288d898151653e6dc" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12203,7 +12203,7 @@ dependencies = [ [[package]] name = "soldering-host" version = "1.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#d1a69cf194e04fc413c6cdd1e8d47b4e834c4ddc" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#f1999e03899ae43d46f191d288d898151653e6dc" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -13889,7 +13889,7 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#d1a69cf194e04fc413c6cdd1e8d47b4e834c4ddc" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#f1999e03899ae43d46f191d288d898151653e6dc" dependencies = [ "aes", "ark-bn254", diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 225c161f..aa8b4d2c 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -18,9 +18,7 @@ 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, WeKnownPi1SetupCt, build_challenge_assert_witness as babe_build_challenge_assert_witness, -}; +use verifiable_circuit_babe::babe::{GC_INPUT_WIRES, build_challenge_assert_witness as babe_build_challenge_assert_witness, WitnessEncSetupCt}; use verifiable_circuit_babe::dre::N_PADDED; use verifiable_circuit_babe::cac::cac_finalize_indices; pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; @@ -130,7 +128,7 @@ pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { let adaptor_tables = [SparseAdaptorTable { entries: vec![] }, SparseAdaptorTable { entries: vec![] }]; - let ct_setup = WeKnownPi1SetupCt { ct2_r_delta_g2: vec![], ct3_masked_msg: vec![] }; + let ct_setup = WitnessEncSetupCt { ct2_r_delta_g2: vec![], ct3_masked_msg: vec![] }; FinalizedInstanceData { index, @@ -214,6 +212,7 @@ pub fn verify_real_setup( finalized: &[FinalizedInstanceData], soldering: &SolderingData, vk: &Groth16VerifyingKey, + claimed_finalized_indices: &[usize], static_public_inputs: Fr, ) -> Result<()> { let bundle = BabeBundle { @@ -222,7 +221,7 @@ pub fn verify_real_setup( soldering: to_real_soldering(soldering)?, }; soldering_builder - .babe_prover_verify_setup(&package, &bundle, vk, static_public_inputs) + .babe_prover_verify_setup(&package, &bundle, vk, static_public_inputs, claimed_finalized_indices) .map_err(anyhow::Error::msg) } diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index 193a490e..b2a6862e 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -66,6 +66,7 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &finalized, &soldering, &vk, + &finalized_indices, static_public_inputs, ) .expect("verify soldering proof"); diff --git a/node/src/handle.rs b/node/src/handle.rs index 3101590d..ba49207e 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -1715,6 +1715,7 @@ async fn handle_compact_soldering_proof_operator( bail!("selected verifier candidate index does not match SolderingProof slot"); } let setup_package = candidate.setup_package.clone(); + let claimed_finalized_indices = candidate.selected_circuit_indexes.clone(); let (opened, finalized, soldering) = expand_compact_soldering_proof_payload(payload) .context("expand compact soldering proof payload")?; @@ -1738,6 +1739,7 @@ async fn handle_compact_soldering_proof_operator( &finalized_for_validation, &soldering_for_validation, &vk, + &claimed_finalized_indices, static_input, ) }) From 1af0dc4fdd8ccc784edab599781d14c27e5b62bf Mon Sep 17 00:00:00 2001 From: KSlashh <48985735+KSlashh@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:48:36 +0800 Subject: [PATCH 12/14] Dev fix (#414) * fix bin * fix prekickoff check & commit pubin logic * update validate graph logic --------- Co-authored-by: ethan --- crates/bitvm-gc/src/babe_adapter.rs | 24 +- crates/bitvm-gc/src/types.rs | 20 +- crates/bitvm-gc/tests/babe_adapter.rs | 5 +- crates/client/src/goat_chain/chain_adaptor.rs | 1 + crates/client/src/goat_chain/evmchain.rs | 4 + crates/client/src/goat_chain/goat_adaptor.rs | 6 + .../src/goat_chain/mock_goat_adaptor.rs | 4 + crates/client/src/goat_chain/mod.rs | 4 + crates/store/src/schema.rs | 1 + node/src/action.rs | 16 + node/src/bin/db_inject.rs | 1 + node/src/bin/send_bridge_out.rs | 1 + node/src/bin/send_challenge.rs | 1 + node/src/bin/send_pegout.rs | 39 +- node/src/bin/send_rbf.rs | 8 +- node/src/bin/sequencer-set-publish.rs | 2 - node/src/handle.rs | 428 +++++++++++++++-- .../src/rpc_service/handler/bitvm2_handler.rs | 21 +- node/src/scheduled_tasks/mod.rs | 3 + node/src/utils.rs | 447 ++++++++++++++++-- 20 files changed, 924 insertions(+), 112 deletions(-) diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index aa8b4d2c..3ee682b6 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -10,18 +10,19 @@ 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::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, build_challenge_assert_witness as babe_build_challenge_assert_witness, WitnessEncSetupCt}; -use verifiable_circuit_babe::dre::N_PADDED; +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; @@ -221,7 +222,13 @@ pub fn verify_real_setup( soldering: to_real_soldering(soldering)?, }; soldering_builder - .babe_prover_verify_setup(&package, &bundle, vk, static_public_inputs, claimed_finalized_indices) + .babe_prover_verify_setup( + &package, + &bundle, + vk, + static_public_inputs, + claimed_finalized_indices, + ) .map_err(anyhow::Error::msg) } @@ -360,10 +367,7 @@ pub fn build_assert_witness( 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 - ) + anyhow::anyhow!("WOTS signature has wrong length; expected {}", Wots96::TOTAL_DIGIT_LEN) })?; Ok(Wots96::signature_to_message(&arr_sig)) } diff --git a/crates/bitvm-gc/src/types.rs b/crates/bitvm-gc/src/types.rs index 5a3fcfec..abb2c1d5 100644 --- a/crates/bitvm-gc/src/types.rs +++ b/crates/bitvm-gc/src/types.rs @@ -705,7 +705,7 @@ impl SimplifiedBitvmGcGraph { } pub fn parameters_hash(&self) -> Result<[u8; 32]> { - self.parameters.parameters_hash() + self.parameters.canonical_graph_params_hash() } } @@ -717,9 +717,23 @@ impl BitvmGcInstanceParameters { } impl BitvmGcGraphParameters { + pub fn canonical_graph_params_hash(&self) -> Result<[u8; 32]> { + let encoded = bincode::serialize(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]> { - let encoded = serde_json::to_vec(self)?; - Ok(Sha256::digest(encoded).into()) + self.canonical_graph_params_hash() + } +} + +impl SimplifiedBitvmGcGraph { + pub fn canonical_graph_params_hash(&self) -> Result<[u8; 32]> { + self.parameters.canonical_graph_params_hash() } } diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index b2a6862e..eed8d250 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -10,8 +10,7 @@ use bitvm_gc::babe_adapter::{ 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, + sample_cac_instance_commit, sample_finalized_instance_data, verify_real_setup, verify_setup, }; use rand::SeedableRng; use rand_chacha::ChaCha12Rng; @@ -356,9 +355,9 @@ fn assert_witness_preserves_pi1_and_dynamic_input() { #[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; - use bitvm::signatures::Wots; // Construct a WOTS sig over bytes that are NOT valid Fq/Fr field elements (all 0xFF). let sk = Wots96::generate_secret_key(); diff --git a/crates/client/src/goat_chain/chain_adaptor.rs b/crates/client/src/goat_chain/chain_adaptor.rs index 3fc6ac00..3984b9be 100644 --- a/crates/client/src/goat_chain/chain_adaptor.rs +++ b/crates/client/src/goat_chain/chain_adaptor.rs @@ -240,6 +240,7 @@ pub trait ChainAdaptor: Send + Sync { ) -> anyhow::Result; async fn peg_btc_balance(&self, address: &[u8; 20]) -> anyhow::Result; + async fn peg_btc_decimals(&self) -> anyhow::Result; } #[derive(Clone, Debug)] diff --git a/crates/client/src/goat_chain/evmchain.rs b/crates/client/src/goat_chain/evmchain.rs index b3c80e82..230542bc 100644 --- a/crates/client/src/goat_chain/evmchain.rs +++ b/crates/client/src/goat_chain/evmchain.rs @@ -461,6 +461,10 @@ impl EvmChain { self.adaptor.peg_btc_balance(address).await } + pub async fn peg_btc_decimals(&self) -> anyhow::Result { + self.adaptor.peg_btc_decimals().await + } + pub async fn peg_btc_allowance( &self, owner: &[u8; 20], diff --git a/crates/client/src/goat_chain/goat_adaptor.rs b/crates/client/src/goat_chain/goat_adaptor.rs index e25b74b1..88880840 100644 --- a/crates/client/src/goat_chain/goat_adaptor.rs +++ b/crates/client/src/goat_chain/goat_adaptor.rs @@ -265,6 +265,7 @@ sol!( #[allow(missing_docs)] #[sol(rpc)] interface IPegBtc{ + function decimals() external view returns (uint8); function balanceOf(address account) external view returns (uint256); function allowance(address owner, address spender) external view returns (uint256); function approve(address spender, uint256 amount) external returns (bool); @@ -1617,6 +1618,11 @@ impl ChainAdaptor for GoatAdaptor { Ok(peg_btc.balanceOf(Address::from_slice(address)).call().await?) } + async fn peg_btc_decimals(&self) -> anyhow::Result { + let peg_btc = self.get_peg_btc()?; + Ok(peg_btc.decimals().call().await?) + } + async fn peg_btc_allowance( &self, owner: &[u8; 20], diff --git a/crates/client/src/goat_chain/mock_goat_adaptor.rs b/crates/client/src/goat_chain/mock_goat_adaptor.rs index e00e1a13..e29a50eb 100644 --- a/crates/client/src/goat_chain/mock_goat_adaptor.rs +++ b/crates/client/src/goat_chain/mock_goat_adaptor.rs @@ -496,6 +496,10 @@ impl ChainAdaptor for MockAdaptor { Ok(U256::default()) } + async fn peg_btc_decimals(&self) -> anyhow::Result { + Ok(18) + } + async fn peg_btc_allowance( &self, _owner: &[u8; 20], diff --git a/crates/client/src/goat_chain/mod.rs b/crates/client/src/goat_chain/mod.rs index e975f568..c2a812f7 100644 --- a/crates/client/src/goat_chain/mod.rs +++ b/crates/client/src/goat_chain/mod.rs @@ -781,6 +781,10 @@ impl GOATClient { self.chain_service.peg_btc_balance(address).await } + pub async fn peg_btc_decimals(&self) -> anyhow::Result { + self.chain_service.peg_btc_decimals().await + } + pub async fn peg_btc_allowance( &self, owner: &[u8; 20], diff --git a/crates/store/src/schema.rs b/crates/store/src/schema.rs index 72c7eb17..f87f671b 100644 --- a/crates/store/src/schema.rs +++ b/crates/store/src/schema.rs @@ -487,6 +487,7 @@ pub enum MessageType { GenCircuits, CutCircuits, SolderingProof, + VerifierGraphParamsEndorsement, NonceGeneration, CommitteePresign, GraphFinalize, diff --git a/node/src/action.rs b/node/src/action.rs index 96095162..4aae08fc 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -45,6 +45,7 @@ pub enum GOATMessageContent { GenCircuits(GenCircuits), CutCircuits(CutCircuits), SolderingProofReady(SolderingProofReady), + VerifierGraphParamsEndorsement(VerifierGraphParamsEndorsement), NonceGeneration(NonceGeneration), CommitteePresign(CommitteePresign), EndorseGraph(EndorseGraph), @@ -121,6 +122,15 @@ pub struct SolderingProofReady { pub total_len: usize, } #[derive(Serialize, Deserialize, Clone)] +pub struct VerifierGraphParamsEndorsement { + pub instance_id: Uuid, + pub graph_id: Uuid, + pub verifier_pubkey: PublicKey, + pub verifier_index: usize, + pub canonical_graph_params_hash: [u8; 32], + pub signature: SchnorrSignature, +} +#[derive(Serialize, Deserialize, Clone)] pub struct CreateGraph { pub instance_id: Uuid, pub graph_id: Uuid, @@ -150,6 +160,7 @@ pub struct EndorseGraph { pub committee_pubkey: PublicKey, pub committee_evm_address: EvmAddress, pub committee_sig_for_graph: Vec, // ECDSA signature signed with committee evm keypair + pub committee_sig_for_params: Vec, // ECDSA signature over canonical_graph_params_hash } #[derive(Serialize, Deserialize, Clone)] pub struct GraphFinalize { @@ -158,6 +169,7 @@ pub struct GraphFinalize { pub graph_nonce: u64, pub graph: SimplifiedBitvmGcGraph, pub endorse_sigs: Vec<(PublicKey, EvmAddress, Vec)>, + pub params_endorse_sigs: Vec<(PublicKey, EvmAddress, Vec)>, } #[derive(Serialize, Deserialize, Clone)] pub struct PeginConfirmNonce { @@ -484,11 +496,14 @@ pub async fn try_finalize_graph( ) -> Result<()> { let endorsements = get_committee_endorsements_for_graph(local_db, instance_id, graph_id).await?; + let params_endorsements = + get_committee_params_endorsements_for_graph(local_db, instance_id, graph_id).await?; let pub_nonoces = get_committee_pub_nonces_for_graph(local_db, instance_id, graph_id).await?; let partial_sigs = get_committee_partial_sigs_for_graph(local_db, instance_id, graph_id).await?; let committee_pubkeys = goat_client.gateway_get_committee_pubkeys(&instance_id).await?; if endorsements.len() == committee_pubkeys.len() + && params_endorsements.len() == committee_pubkeys.len() && pub_nonoces.len() == committee_pubkeys.len() && partial_sigs.len() == committee_pubkeys.len() { @@ -519,6 +534,7 @@ pub async fn try_finalize_graph( graph_id, graph_nonce: graph.parameters.graph_nonce, endorse_sigs: endorsements, + params_endorse_sigs: params_endorsements, graph: simplified_graph, }); send_to_peer(swarm, GOATMessage::new(Actor::All, message_content)).await?; diff --git a/node/src/bin/db_inject.rs b/node/src/bin/db_inject.rs index 4a7e9cc2..07abf24a 100644 --- a/node/src/bin/db_inject.rs +++ b/node/src/bin/db_inject.rs @@ -84,6 +84,7 @@ fn infer_business_id(content: &GOATMessageContent) -> Option { GOATMessageContent::GenCircuits(v) => Some(v.instance_id), GOATMessageContent::CutCircuits(v) => Some(v.instance_id), GOATMessageContent::SolderingProofReady(v) => Some(v.instance_id), + GOATMessageContent::VerifierGraphParamsEndorsement(v) => Some(v.graph_id), GOATMessageContent::CreateGraph(v) => Some(v.graph_id), GOATMessageContent::NonceGeneration(v) => Some(v.graph_id), GOATMessageContent::CommitteePresign(v) => Some(v.graph_id), diff --git a/node/src/bin/send_bridge_out.rs b/node/src/bin/send_bridge_out.rs index 583727ab..38930150 100644 --- a/node/src/bin/send_bridge_out.rs +++ b/node/src/bin/send_bridge_out.rs @@ -739,6 +739,7 @@ async fn call_get_escrow_data( #[tokio::main] async fn main() -> Result<()> { + dotenv::dotenv().ok(); let args = Args::parse(); let client = reqwest::Client::new(); diff --git a/node/src/bin/send_challenge.rs b/node/src/bin/send_challenge.rs index 31abeb4c..c283e1fe 100644 --- a/node/src/bin/send_challenge.rs +++ b/node/src/bin/send_challenge.rs @@ -48,6 +48,7 @@ struct SendChallengeResponse { #[tokio::main] async fn main() -> Result<()> { + dotenv::dotenv().ok(); let args = Args::parse(); let url = format!( "{}/v1/graphs/{}/send-challenge", diff --git a/node/src/bin/send_pegout.rs b/node/src/bin/send_pegout.rs index 77e9270d..10de16d9 100644 --- a/node/src/bin/send_pegout.rs +++ b/node/src/bin/send_pegout.rs @@ -202,6 +202,7 @@ async fn wait_for_graph_ready( #[tokio::main] async fn main() -> Result<()> { + dotenv::dotenv().ok(); let args = Args::parse(); let client = reqwest::Client::new(); let base_url = &args.rpc_url; @@ -235,31 +236,41 @@ async fn main() -> Result<()> { break; } - match call_pegout(&client, base_url, None, dry_run).await { - Ok(resp) => { - let amount = resp.amount as u64; - if total_sent + amount > max_total_amount_sats { + match call_pegout(&client, base_url, None, true).await { + Ok(preflight) => { + let amount = u64::try_from(preflight.amount) + .context("pegout API returned a negative amount")?; + if total_sent.saturating_add(amount) > max_total_amount_sats { eprintln!( "batch stop: next amount {amount} would exceed target {max_total_amount_sats}", ); break; } + if dry_run { + print_pegout_result(&preflight); + eprintln!( + "batch dry-run stops after the next eligible graph because dry-run does not advance graph state" + ); + break; + } + + let graph_id = Uuid::parse_str(&preflight.graph_id) + .context("pegout API returned an invalid graph id")?; + let resp = call_pegout(&client, base_url, Some(graph_id), false).await?; print_pegout_result(&resp); total_sent = total_sent.saturating_add(amount); count = count.saturating_add(1); eprintln!("batch progress: count {count}, total {total_sent} sats"); - if !dry_run { - wait_for_graph_ready( - &client, - base_url, - &resp.graph_id, - poll_interval_secs, - max_wait_secs, - ) - .await?; - } + wait_for_graph_ready( + &client, + base_url, + &resp.graph_id, + poll_interval_secs, + max_wait_secs, + ) + .await?; } Err(e) => { eprintln!("batch stop: {e}"); diff --git a/node/src/bin/send_rbf.rs b/node/src/bin/send_rbf.rs index 861a37f5..369fbdc3 100644 --- a/node/src/bin/send_rbf.rs +++ b/node/src/bin/send_rbf.rs @@ -7,7 +7,7 @@ //! //! Env: //! - BITVM_SECRET: node BTC private key -//! - BITCOIN_NETWORK: bitcoin | testnet | testnet4 | signet | regtest (optional) +//! - BITCOIN_NETWORK: bitcoin | testnet4 | signet | regtest (optional) //! //! Example: //! - cargo run -p bitvm-noded --bin send-rbf -- \ @@ -56,8 +56,8 @@ struct Args { fee_amount: u64, /// Optional esplora base URL override - #[arg(long, default_value = "https://mempool.space/testnet4/api")] - esplora_url: String, + #[arg(long)] + esplora_url: Option, } #[derive(Clone, Debug)] @@ -94,7 +94,7 @@ async fn main() -> Result<()> { let args = Args::parse(); let network = get_network(); - let btc_client = BTCClient::new(network, Some(&args.esplora_url)); + let btc_client = BTCClient::new(network, args.esplora_url.as_deref()); let node_keypair = get_bitvm_key()?; let node_address = node_p2wsh_address(network, &node_keypair.public_key().into()); diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index 1a8d2caf..f715eddf 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -274,7 +274,6 @@ async fn main() -> Result<(), Box> { dotenv().ok(); let _ = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).try_init(); let args = Args::parse(); - println!("args: {args:?}"); let (btc_client, goat_client) = init_clients(&args).await?; let output_file = &args.output_file; @@ -293,7 +292,6 @@ async fn main() -> Result<(), Box> { btc_key_wifs.iter().for_each(|btc_key_wif| { let secp = secp256k1::Secp256k1::new(); let private_key = PrivateKey::from_wif(btc_key_wif).expect("Invalid BTC WIF Key"); - println!("Hex Private Key: {}", private_key.inner.display_secret()); println!("Public Key: {}", private_key.public_key(&secp)); }); Ok(()) diff --git a/node/src/handle.rs b/node/src/handle.rs index ba49207e..480b43a9 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -236,8 +236,35 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent } ( GOATMessageContent::CreateGraph(CreateGraph { instance_id, graph_id, graph, .. }), + Actor::Verifier, + ) => handle_create_graph_verifier(ctx, *instance_id, *graph_id, graph).await, + ( + GOATMessageContent::CreateGraph(CreateGraph { instance_id, graph_id, graph, .. }), + Actor::Committee, + ) => handle_create_graph_committee(ctx, *instance_id, *graph_id, graph, content).await, + ( + GOATMessageContent::VerifierGraphParamsEndorsement(VerifierGraphParamsEndorsement { + instance_id, + graph_id, + verifier_pubkey, + verifier_index, + canonical_graph_params_hash, + signature, + }), Actor::Committee, - ) => handle_create_graph_committee(ctx, *instance_id, *graph_id, graph).await, + ) => { + handle_verifier_graph_params_endorsement_committee( + ctx, + *instance_id, + *graph_id, + verifier_pubkey, + *verifier_index, + *canonical_graph_params_hash, + signature, + content, + ) + .await + } ( GOATMessageContent::NonceGeneration(NonceGeneration { instance_id, @@ -327,6 +354,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent graph_id, committee_pubkey: received_committee_pubkey, committee_sig_for_graph, + committee_sig_for_params, committee_evm_address, }), Actor::Operator, @@ -337,6 +365,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent *graph_id, received_committee_pubkey, committee_sig_for_graph, + committee_sig_for_params, committee_evm_address, ) .await @@ -347,11 +376,20 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent graph_id, graph, endorse_sigs, + params_endorse_sigs, .. }), Actor::Committee, ) => { - handle_graph_finalize_committee(ctx, *instance_id, *graph_id, graph, endorse_sigs).await + handle_graph_finalize_committee( + ctx, + *instance_id, + *graph_id, + graph, + endorse_sigs, + params_endorse_sigs, + ) + .await } ( GOATMessageContent::GraphFinalize(GraphFinalize { @@ -359,10 +397,21 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent graph_id, graph, endorse_sigs, + params_endorse_sigs, .. }), _, - ) => handle_graph_finalize_default(ctx, *instance_id, *graph_id, graph, endorse_sigs).await, + ) => { + handle_graph_finalize_default( + ctx, + *instance_id, + *graph_id, + graph, + endorse_sigs, + params_endorse_sigs, + ) + .await + } ( GOATMessageContent::PeginConfirmNonce(PeginConfirmNonce { instance_id, @@ -1835,23 +1884,126 @@ async fn handle_confirm_instance_default( } #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_create_graph_committee( +async fn handle_create_graph_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, graph: &SimplifiedBitvmGcGraph, ) -> Result<()> { - // received from Operator - // 1. check graph data & operator stake - if let Err(e) = todo_funcs::validate_init_graph(ctx.btc_client, ctx.goat_client, graph).await { + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let local_verifier_pubkey: PublicKey = verifier_master_key.master_keypair().public_key().into(); + let Some(verifier_index) = + find_verifier_index_by_pubkey(&graph.parameters.gc_data, &local_verifier_pubkey)? + else { + return Ok(()); + }; + + let full_graph = BitvmGcGraph::from_simplified(graph)?; + validate_verifier_slot(&full_graph, verifier_index)?; + let Some(verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? + .and_then(|state| state.verifier) + else { + tracing::warn!( + "Ignore CreateGraph for {instance_id}:{graph_id}: missing local BABE verifier state" + ); + return Ok(()); + }; + if verifier_state.verifier_pubkey != local_verifier_pubkey { + tracing::warn!( + "Ignore CreateGraph for {instance_id}:{graph_id}: local BABE verifier pubkey mismatch" + ); + return Ok(()); + } + let Some(soldering_proof_ready) = verifier_state.soldering_proof_ready.clone() else { + tracing::warn!( + "Ignore CreateGraph for {instance_id}:{graph_id}: local verifier has no soldering proof" + ); + return Ok(()); + }; + if soldering_proof_ready.verifier_index != verifier_index { + bail!( + "CreateGraph verifier index {verifier_index} conflicts with persisted slot {}", + soldering_proof_ready.verifier_index + ); + } + + let store_base_path = get_soldering_proof_payload_store_path()?; + let payload_path = soldering_proof_payload_store_path( + &store_base_path, + instance_id, + graph_id, + verifier_index, + &soldering_proof_ready.payload_hash, + )?; + let payload = read_soldering_proof_store_payload(&payload_path) + .await + .context("read local soldering proof payload for graph params endorsement")?; + let payload = decode_soldering_proof_payload(&soldering_proof_ready, &payload)?; + let (_opened, finalized, soldering) = expand_compact_soldering_proof_payload(payload) + .context("expand local soldering proof payload for graph params endorsement")?; + if finalized.len() != BABE_M_CC { + bail!("local verifier finalized BABE instance count mismatch"); + } + let epk = &verifier_state.setup_package.commits[finalized[0].index].epk; + let prover_state = + build_babe_prover_state(&verifier_state.setup_package, finalized, soldering)?; + let expected_gc_data = + extract_gc_circuit_data(local_verifier_pubkey, epk, &prover_state.h_msgs)?; + if graph.parameters.gc_data[verifier_index] != expected_gc_data { + tracing::warn!( + "Ignore CreateGraph for {instance_id}:{graph_id}: graph GC data for local verifier does not match local soldering proof" + ); + return Ok(()); + } + + let canonical_graph_params_hash = graph.canonical_graph_params_hash()?; + let signature = sign_verifier_graph_params(verifier_master_key.master_keypair(), graph)?; + let message_content = + GOATMessageContent::VerifierGraphParamsEndorsement(VerifierGraphParamsEndorsement { + instance_id, + graph_id, + verifier_pubkey: local_verifier_pubkey, + verifier_index, + canonical_graph_params_hash, + signature, + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::Committee, message_content)).await?; + Ok(()) +} + +async fn try_start_graph_committee_setup( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + graph: &SimplifiedBitvmGcGraph, +) -> Result<()> { + let verifier_endorsements = + get_verifier_graph_params_endorsements_for_graph(ctx.local_db, instance_id, graph_id) + .await?; + if verifier_endorsements.len() < graph.parameters.gc_data.len() { + tracing::info!( + "Defer CreateGraph for {instance_id}:{graph_id}: waiting for verifier graph params endorsements ({}/{})", + verifier_endorsements.len(), + graph.parameters.gc_data.len() + ); + return Ok(()); + } + if let Err(e) = todo_funcs::validate_init_graph( + ctx.local_db, + ctx.btc_client, + ctx.goat_client, + graph, + &verifier_endorsements, + ) + .await + { if should_ignore_invalid_graph(&e, instance_id, graph_id, "CreateGraph", None) { return Ok(()); } bail!(e) }; - // 2. save the graph data to local db - store_graph(ctx.local_db, graph).await?; - // 3. generate Musig2 nonces & broadcast NonceGeneration + + // generate Musig2 nonces & broadcast NonceGeneration let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; let verifier_num = graph.parameters.gc_data.len(); @@ -1896,7 +2048,7 @@ async fn handle_create_graph_committee( pub_nonces, ) .await?; - // 4. if collected enough pub_nonces, generate partial signatures & broadcast CommitteePresign + // if collected enough pub_nonces, generate partial signatures & broadcast CommitteePresign let committee_pubkeys = ctx.goat_client.gateway_get_committee_pubkeys(&instance_id).await?; let pub_nonces_unchecked = get_committee_pub_nonces_for_graph(ctx.local_db, instance_id, graph_id).await?; @@ -1965,6 +2117,144 @@ async fn handle_create_graph_committee( Ok(()) } +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_create_graph_committee( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + graph: &SimplifiedBitvmGcGraph, + content: &GOATMessageContent, +) -> Result<()> { + // received from Operator + if graph.parameters.graph_nonce > 0 { + let previous_nonce = graph.parameters.graph_nonce - 1; + match get_graph_id_by_nonce(ctx.local_db, previous_nonce, &graph.parameters.operator_pubkey) + .await? + { + Some((previous_instance_id, previous_graph_id)) => { + if get_graph(ctx.local_db, previous_instance_id, previous_graph_id).await?.is_none() + { + if let Err(e) = try_send_sync_graph_request( + ctx.swarm, + ctx.goat_client, + previous_instance_id, + previous_graph_id, + ) + .await + { + tracing::warn!( + "Failed to send SyncGraphRequest for previous graph {previous_instance_id}:{previous_graph_id}: {e}" + ); + } + let message = make_message(ctx, content); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, 60).await?; + tracing::info!( + "Defer CreateGraph for {instance_id}:{graph_id}: waiting for previous graph raw data {previous_instance_id}:{previous_graph_id}" + ); + return Ok(()); + } + } + None => { + let message = make_message(ctx, content); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, 60).await?; + tracing::info!( + "Defer CreateGraph for {instance_id}:{graph_id}: previous graph with nonce {previous_nonce} is not available locally" + ); + return Ok(()); + } + } + } + + // 1. check graph data & operator stake without verifier params endorsements; those may arrive later. + if let Err(e) = + todo_funcs::validate_init_graph_base(ctx.local_db, ctx.btc_client, ctx.goat_client, graph) + .await + { + if should_ignore_invalid_graph(&e, instance_id, graph_id, "CreateGraph", None) { + return Ok(()); + } + bail!(e) + }; + // 2. save the graph data to local db + store_graph(ctx.local_db, graph).await?; + // 3. start committee setup once verifier params endorsements are complete + try_start_graph_committee_setup(ctx, instance_id, graph_id, graph).await +} + +#[allow(clippy::too_many_arguments)] +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_verifier_graph_params_endorsement_committee( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + verifier_pubkey: &PublicKey, + verifier_index: usize, + canonical_graph_params_hash: [u8; 32], + signature: &secp256k1::schnorr::Signature, + content: &GOATMessageContent, +) -> Result<()> { + if !ctx.is_self_peer + && !ctx.goat_client.committee_mana_is_verifier(&ctx.from_peer_id.to_bytes()).await? + { + tracing::warn!( + "Ignore VerifierGraphParamsEndorsement for {instance_id}:{graph_id}: sender {} is not a registered verifier", + ctx.from_peer_id + ); + return Ok(()); + } + + let message = make_message(ctx, content); + let graph = match get_graph_or_defer( + ctx.swarm, + ctx.local_db, + ctx.goat_client, + instance_id, + graph_id, + &message, + ) + .await? + { + Some(graph) => graph, + None => return Ok(()), + }; + let expected_hash = graph.canonical_graph_params_hash()?; + if canonical_graph_params_hash != expected_hash { + tracing::warn!( + "Ignore VerifierGraphParamsEndorsement for {instance_id}:{graph_id} from {verifier_pubkey}: canonical graph params hash mismatch" + ); + return Ok(()); + } + if verifier_index >= graph.parameters.gc_data.len() { + tracing::warn!( + "Ignore VerifierGraphParamsEndorsement for {instance_id}:{graph_id} from {verifier_pubkey}: verifier index {verifier_index} out of range" + ); + return Ok(()); + } + if graph.parameters.gc_data[verifier_index].verifier_pubkey != *verifier_pubkey { + tracing::warn!( + "Ignore VerifierGraphParamsEndorsement for {instance_id}:{graph_id}: verifier pubkey does not own slot {verifier_index}" + ); + return Ok(()); + } + if !verify_verifier_graph_params_endorsement(verifier_pubkey, &graph, signature)? { + tracing::warn!( + "Ignore VerifierGraphParamsEndorsement for {instance_id}:{graph_id} from {verifier_pubkey}: invalid signature" + ); + return Ok(()); + } + + store_verifier_graph_params_endorsement( + ctx.local_db, + instance_id, + graph_id, + *verifier_pubkey, + verifier_index, + signature.clone(), + ) + .await?; + try_start_graph_committee_setup(ctx, instance_id, graph_id, &graph).await +} + #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_nonce_generation_committee( ctx: &mut HandlerContext<'_>, @@ -2103,6 +2393,7 @@ async fn handle_nonce_generation_committee( .collect::>(); if committee_partial_sigs.len() == committee_pubkeys.len() { let committee_sig_for_graph = endorse_graph(ctx.goat_client, &graph).await?; + let committee_sig_for_params = endorse_graph_params(&graph).await?; let committee_evm_address = get_node_goat_address() .ok_or_else(|| anyhow::anyhow!("failed to get node goat address".to_string()))?; let message_content = GOATMessageContent::EndorseGraph(EndorseGraph { @@ -2110,6 +2401,7 @@ async fn handle_nonce_generation_committee( graph_id, committee_pubkey: local_committee_pubkey, committee_sig_for_graph: committee_sig_for_graph.as_bytes().to_vec(), + committee_sig_for_params: committee_sig_for_params.as_bytes().to_vec(), committee_evm_address, }); send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; @@ -2341,6 +2633,7 @@ async fn handle_committee_presign_committee( .collect::>(); if committee_partial_sigs.len() == committee_pubkeys.len() { let committee_sig_for_graph = endorse_graph(ctx.goat_client, &graph).await?; + let committee_sig_for_params = endorse_graph_params(&graph).await?; let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; let local_committee_pubkey = instance_keypair.public_key().into(); @@ -2351,6 +2644,7 @@ async fn handle_committee_presign_committee( graph_id, committee_pubkey: local_committee_pubkey, committee_sig_for_graph: committee_sig_for_graph.as_bytes().to_vec(), + committee_sig_for_params: committee_sig_for_params.as_bytes().to_vec(), committee_evm_address, }); send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; @@ -2418,6 +2712,7 @@ async fn handle_endorse_graph_operator( graph_id: Uuid, received_committee_pubkey: &PublicKey, committee_sig_for_graph: &[u8], + committee_sig_for_params: &[u8], committee_evm_address: &alloy::primitives::Address, ) -> Result<()> { // received from Committee members @@ -2459,6 +2754,27 @@ async fn handle_endorse_graph_operator( ); return Ok(()); } + match verify_graph_params_endorsement( + committee_evm_address, + &full_graph, + committee_sig_for_params, + ) { + Ok(true) => {} + Ok(false) => { + tracing::warn!( + "Ignore EndorseGraph for {instance_id}:{graph_id} from {}: invalid params endorsement signature", + received_committee_pubkey.to_string() + ); + return Ok(()); + } + Err(e) => { + tracing::warn!( + "Ignore EndorseGraph for {instance_id}:{graph_id} from {}: invalid params endorsement signature: {e}", + received_committee_pubkey.to_string() + ); + return Ok(()); + } + } // 2. save the endorsement signature to local db store_committee_endorsement_for_graph( ctx.local_db, @@ -2467,6 +2783,7 @@ async fn handle_endorse_graph_operator( *received_committee_pubkey, *committee_evm_address, committee_sig_for_graph.to_owned(), + committee_sig_for_params.to_owned(), ) .await?; // 3. if received enough endorsement signatures, mark the graph as endorsed, send the graph to local database, broadcast GraphFinalize @@ -2492,12 +2809,18 @@ async fn handle_graph_finalize_committee( graph_id: Uuid, graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], + params_endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], ) -> Result<()> { // received from Operator // 1. check graph data - if let Err(e) = - todo_funcs::validate_finalized_graph(ctx.btc_client, ctx.goat_client, graph, endorse_sigs) - .await + if let Err(e) = todo_funcs::validate_finalized_graph( + ctx.btc_client, + ctx.goat_client, + graph, + endorse_sigs, + params_endorse_sigs, + ) + .await { if should_ignore_invalid_graph( &e, @@ -2517,6 +2840,7 @@ async fn handle_graph_finalize_committee( instance_id, graph_id, endorse_sigs.to_owned(), + params_endorse_sigs.to_owned(), ) .await?; // After storing, mark the graph as endorsed @@ -2602,12 +2926,18 @@ async fn handle_graph_finalize_default( graph_id: Uuid, graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], + params_endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], ) -> Result<()> { // received from Operator // 1. check graph data - if let Err(e) = - todo_funcs::validate_finalized_graph(ctx.btc_client, ctx.goat_client, graph, endorse_sigs) - .await + if let Err(e) = todo_funcs::validate_finalized_graph( + ctx.btc_client, + ctx.goat_client, + graph, + endorse_sigs, + params_endorse_sigs, + ) + .await { if should_ignore_invalid_graph( &e, @@ -3299,6 +3629,7 @@ async fn handle_kickoff_sent_verifier( return Ok(()); } }; + handle_previous_graph_after_prekickoff(ctx, instance_id, graph_id, &graph, &message).await?; let take1_txid = graph.take1.tx().compute_txid(); let (challenge_tx, _) = export_challenge_tx(&graph).unwrap(); let kickoff_challenge_outpoint = challenge_tx.input[0].previous_output; @@ -3359,28 +3690,13 @@ async fn handle_kickoff_sent_default( Ok(()) } -#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] -async fn handle_prekickoff_sent_verifier( +async fn handle_previous_graph_after_prekickoff( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - content: &GOATMessageContent, + graph: &BitvmGcGraph, + message: &GOATMessage, ) -> Result<()> { - // triggered by PreKickoff tx - let message = make_message(ctx, content); - let (graph, _graph_status, _graph_sub_status) = match refresh_graph_status( - ctx, - instance_id, - graph_id, - Some(&message), - GraphStatus::PreKickoff, - ) - .await? - { - Some(v) => v, - None => return Ok(()), - }; - // 1. check the previous graph status if !tx_on_chain( ctx.btc_client, &graph.parameters.prekickoff_parameters.cur_prekickoff_txn.tx().compute_txid(), @@ -3388,14 +3704,16 @@ async fn handle_prekickoff_sent_verifier( .await? { tracing::warn!( - "Ignore PreKickoffSent for {instance_id}:{graph_id}: prekickoff tx not on chain" + "Ignore prekickoff follow-up for {instance_id}:{graph_id}: prekickoff tx not on chain" ); return Ok(()); } + let graph_nonce = graph.parameters.graph_nonce; if graph_nonce == 0 { return Ok(()); } + let (prev_instance_id, prev_graph_id) = get_graph_id_by_nonce(ctx.local_db, graph_nonce - 1, &graph.parameters.operator_pubkey) .await? @@ -3406,7 +3724,7 @@ async fn handle_prekickoff_sent_verifier( ctx.goat_client, prev_instance_id, prev_graph_id, - &message, + message, ) .await? { @@ -3427,15 +3745,39 @@ async fn handle_prekickoff_sent_verifier( ) .await?; if !tx_on_chain(ctx.btc_client, &prev_graph.kickoff.tx().compute_txid()).await? { - // 2. if previous kickoff not started, broadcast force-skip-kickoff txn verifier_force_skip_kickoff(ctx.btc_client, &prev_graph).await?; } else if !prev_graph_status.is_closed() { - // 3. if previous kickoff is not closed, broadcast quick-challenge/challenge-incomplete-kickoff txn verifier_quick_challenge(ctx.btc_client, &prev_graph).await?; } Ok(()) } +#[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] +async fn handle_prekickoff_sent_verifier( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + content: &GOATMessageContent, +) -> Result<()> { + // triggered by PreKickoff tx + let message = make_message(ctx, content); + let (graph, _graph_status, _graph_sub_status) = match refresh_graph_status( + ctx, + instance_id, + graph_id, + Some(&message), + GraphStatus::PreKickoff, + ) + .await? + { + Some(v) => v, + None => return Ok(()), + }; + // 1. check the previous graph status + handle_previous_graph_after_prekickoff(ctx, instance_id, graph_id, &graph, &message).await?; + Ok(()) +} + #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_prekickoff_sent_default( ctx: &mut HandlerContext<'_>, @@ -3904,7 +4246,11 @@ async fn handle_operator_commit_pubin_ready_operator( let watchtower_timeout_txids: Vec = graph.watchtower_challenge_timeouts.iter().map(|tx| tx.tx().compute_txid()).collect(); let wait_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()) as usize; - let (challenge_txids, included_watchtowers_bits) = match get_watchtower_challenge_info( + let WatchtowerChallengeInfo { + included_watchtowers: included_watchtowers_bits, + resolved_branch_txids, + .. + } = match get_watchtower_challenge_info( ctx.btc_client, &watchtower_challenge_init_txid, &watchtower_timeout_txids, @@ -3924,7 +4270,7 @@ async fn handle_operator_commit_pubin_ready_operator( let (btc_best_block_hash, included_watchtowers_bitmap) = match compute_operator_pubin_blockhash_and_bitmap( ctx.btc_client, - &challenge_txids, + &resolved_branch_txids, &included_watchtowers_bits, ) .await diff --git a/node/src/rpc_service/handler/bitvm2_handler.rs b/node/src/rpc_service/handler/bitvm2_handler.rs index f3a6c5ae..ad4b9d90 100644 --- a/node/src/rpc_service/handler/bitvm2_handler.rs +++ b/node/src/rpc_service/handler/bitvm2_handler.rs @@ -1645,6 +1645,16 @@ pub async fn send_challenge( ok_response(SendChallengeResponse { challenge_txid: txid.to_string() }) } +const BTC_DECIMALS: u8 = 8; + +fn sats_to_token_amount(amount_sats: u64, token_decimals: u8) -> U256 { + if token_decimals >= BTC_DECIMALS { + U256::from(amount_sats) * U256::from(10).pow(U256::from(token_decimals - BTC_DECIMALS)) + } else { + U256::from(amount_sats / 10u64.pow(u32::from(BTC_DECIMALS - token_decimals))) + } +} + /// Initiate operator pegout (Gateway.initWithdraw) /// /// Selects an eligible graph (or uses the provided graph_id), checks L2 pegin/withdraw @@ -1817,8 +1827,10 @@ pub async fn pegout( }); } - // Ensure pegBTC allowance - let amount_u256 = U256::from(amount as u64); + // Mirror Gateway._amountFromSats using the on-chain pegin amount and pegBTC decimals. + let peg_btc_decimals = + app_state.goat_client.peg_btc_decimals().await.api_error("PEGOUT_ERROR")?; + let amount_u256 = sats_to_token_amount(pegin_data.pegin_amount_sats, peg_btc_decimals); let balance = app_state .goat_client .peg_btc_balance(&operator_goat_addr.0) @@ -1827,7 +1839,10 @@ pub async fn pegout( if balance < amount_u256 { return error_response( "PEGOUT_ERROR".to_string(), - format!("insufficient pegBTC balance: need {amount}, available {balance}"), + format!( + "insufficient pegBTC balance: need {amount_u256} base units ({} sats, {peg_btc_decimals} decimals), available {balance}", + pegin_data.pegin_amount_sats + ), ); } diff --git a/node/src/scheduled_tasks/mod.rs b/node/src/scheduled_tasks/mod.rs index e590752e..c9432496 100644 --- a/node/src/scheduled_tasks/mod.rs +++ b/node/src/scheduled_tasks/mod.rs @@ -175,6 +175,9 @@ pub fn get_goat_message_content_type(content: &GOATMessageContent) -> MessageTyp GOATMessageContent::GenCircuits(_) => MessageType::GenCircuits, GOATMessageContent::CutCircuits(_) => MessageType::CutCircuits, GOATMessageContent::SolderingProofReady(_) => MessageType::SolderingProof, + GOATMessageContent::VerifierGraphParamsEndorsement(_) => { + MessageType::VerifierGraphParamsEndorsement + } GOATMessageContent::NonceGeneration(_) => MessageType::NonceGeneration, GOATMessageContent::CommitteePresign(_) => MessageType::CommitteePresign, GOATMessageContent::GraphFinalize(_) => MessageType::GraphFinalize, diff --git a/node/src/utils.rs b/node/src/utils.rs index 3823fcbb..da6f792f 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -60,7 +60,8 @@ use p3_bn254_fr::Bn254Fr; use p3_field::{FieldAlgebra, PrimeField}; use rand::Rng; use reqwest::Url; -use secp256k1::Secp256k1; +use secp256k1::schnorr::Signature as SchnorrSignature; +use secp256k1::{Message as SecpMessage, SECP256K1, Secp256k1}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::net::SocketAddr; @@ -85,7 +86,7 @@ use crate::scheduled_tasks::graph_maintenance_tasks::{ }; use bitcoin_light_client_circuit::hash_operator_constant; use bitvm_lib::babe_adapter::{ - BabeVerifierPrivateState, CACSetupPackage, FinalizedInstanceData, SolderingData, + BABE_M_CC, BabeVerifierPrivateState, CACSetupPackage, FinalizedInstanceData, SolderingData, compact_soldering_proof_payload, }; use bitvm_lib::transactions::base::BaseTransaction; @@ -281,7 +282,110 @@ pub mod todo_funcs { Ok(()) } - pub async fn validate_init_graph( + fn validate_graph_policy(parameters: &BitvmGcGraphParameters) -> Result<()> { + let verifier_num = parameters.gc_data.len(); + if verifier_num < min_required_verifier() { + bail!(SpecialError::InvalidGraph(format!( + "insufficient verifier GC slots: have {}, required {}", + verifier_num, + min_required_verifier() + ))); + } + + let mut verifier_pubkeys = std::collections::HashSet::new(); + for gc_data in ¶meters.gc_data { + if !verifier_pubkeys.insert(gc_data.verifier_pubkey) { + bail!(SpecialError::InvalidGraph(format!( + "duplicate verifier pubkey in GC data: {}", + gc_data.verifier_pubkey + ))); + } + if gc_data.final_msg_hashlocks.len() != BABE_M_CC { + bail!(SpecialError::InvalidGraph(format!( + "verifier {} has {} final message hashlocks, expected {}", + gc_data.verifier_pubkey, + gc_data.final_msg_hashlocks.len(), + BABE_M_CC + ))); + } + } + + let watchtower_num = parameters.watchtower_pubkeys.len(); + if watchtower_num < min_required_watchtower() { + bail!(SpecialError::InvalidGraph(format!( + "insufficient watchtower slots: have {}, required {}", + watchtower_num, + min_required_watchtower() + ))); + } + let mut watchtower_pubkeys = std::collections::HashSet::new(); + for pubkey in ¶meters.watchtower_pubkeys { + if !watchtower_pubkeys.insert(*pubkey) { + bail!(SpecialError::InvalidGraph(format!( + "duplicate watchtower pubkey in graph: {pubkey}" + ))); + } + } + let mut watchtower_hashlocks = std::collections::HashSet::new(); + for hashlock in ¶meters.watchtower_ack_hashlocks { + if !watchtower_hashlocks.insert(*hashlock) { + bail!(SpecialError::InvalidGraph( + "duplicate watchtower ack hashlock in graph".to_string() + )); + } + } + + Ok(()) + } + + async fn validate_prekickoff_continuity( + local_db: &LocalDB, + full_graph: &BitvmGcGraph, + ) -> Result<()> { + if full_graph.parameters.graph_nonce == 0 { + return Ok(()); + } + + let mut storage_processor = local_db.acquire().await?; + let previous_nonce = full_graph.parameters.graph_nonce - 1; + let graphs = storage_processor + .get_operator_graphs( + GraphQuery::default() + .with_operator_pubkey(full_graph.parameters.operator_pubkey.to_string()) + .with_kickoff_index(previous_nonce as i64) + .with_limit(2), + ) + .await?; + if let Some(previous_graph) = graphs.first() { + let Some(graph_raw_data) = + storage_processor.find_graph_raw_data(&previous_graph.graph_id).await? + else { + bail!(SpecialError::InvalidGraph(format!( + "previous graph raw data is missing for graph nonce {previous_nonce}" + ))); + }; + let simplified_graph = + super::parse_graph_raw_data(graph_raw_data.raw_data, previous_graph.graph_id) + .await?; + let expected_cur_prekickoff = + BitvmGcGraph::from_simplified(&simplified_graph)?.next_prekickoff; + let expected_txid = expected_cur_prekickoff.finalize().compute_txid(); + let actual_txid = full_graph.cur_prekickoff.finalize().compute_txid(); + if actual_txid != expected_txid { + bail!(SpecialError::InvalidGraph(format!( + "cur prekickoff continuity mismatch: graph={actual_txid}, expected={expected_txid}" + ))); + } + Ok(()) + } else { + bail!(SpecialError::InvalidGraph(format!( + "previous graph is missing for graph nonce {previous_nonce}" + ))); + } + } + + pub async fn validate_init_graph_base( + local_db: &LocalDB, btc_client: &BTCClient, goat_client: &GOATClient, graph: &SimplifiedBitvmGcGraph, @@ -296,6 +400,8 @@ pub mod todo_funcs { // 1) Rebuild full graph (ensures signatures present if flags are set and tx graph is coherent) let full_graph = BitvmGcGraph::from_simplified(graph) .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + validate_graph_policy(&graph.parameters)?; + validate_prekickoff_continuity(local_db, &full_graph).await?; verify_graph_operator_pre_signatures(&full_graph).map_err(|e| { SpecialError::InvalidGraph(format!("invalid operator pre-signatures: {e}")) })?; @@ -354,11 +460,74 @@ pub mod todo_funcs { Ok(()) } + + pub fn validate_verifier_graph_params_endorsements( + graph: &SimplifiedBitvmGcGraph, + verifier_endorsements: &[(PublicKey, usize, SchnorrSignature)], + ) -> Result<()> { + let expected_verifier_num = graph.parameters.gc_data.len(); + if verifier_endorsements.len() != expected_verifier_num { + bail!(SpecialError::InvalidGraph(format!( + "verifier params endorsement count {} does not match GC slot count {}", + verifier_endorsements.len(), + expected_verifier_num + ))); + } + + let mut seen_pubkeys = std::collections::HashSet::new(); + let mut seen_indices = std::collections::HashSet::new(); + for (verifier_pubkey, verifier_index, signature) in verifier_endorsements { + if *verifier_index >= expected_verifier_num { + bail!(SpecialError::InvalidGraph(format!( + "verifier params endorsement index {verifier_index} out of range" + ))); + } + let slot_pubkey = graph.parameters.gc_data[*verifier_index].verifier_pubkey; + if slot_pubkey != *verifier_pubkey { + bail!(SpecialError::InvalidGraph(format!( + "verifier params endorsement slot mismatch at index {verifier_index}: signature pubkey={}, graph pubkey={slot_pubkey}", + verifier_pubkey + ))); + } + if !seen_pubkeys.insert(*verifier_pubkey) { + bail!(SpecialError::InvalidGraph(format!( + "duplicate verifier params endorsement pubkey: {verifier_pubkey}" + ))); + } + if !seen_indices.insert(*verifier_index) { + bail!(SpecialError::InvalidGraph(format!( + "duplicate verifier params endorsement index: {verifier_index}" + ))); + } + let ok = + super::verify_verifier_graph_params_endorsement(verifier_pubkey, graph, signature)?; + if !ok { + bail!(SpecialError::InvalidGraph(format!( + "invalid verifier params endorsement signature for {verifier_pubkey}" + ))); + } + } + + Ok(()) + } + + pub async fn validate_init_graph( + local_db: &LocalDB, + btc_client: &BTCClient, + goat_client: &GOATClient, + graph: &SimplifiedBitvmGcGraph, + verifier_endorsements: &[(PublicKey, usize, SchnorrSignature)], + ) -> Result<()> { + validate_init_graph_base(local_db, btc_client, goat_client, graph).await?; + validate_verifier_graph_params_endorsements(graph, verifier_endorsements)?; + Ok(()) + } pub async fn validate_finalized_graph( btc_client: &BTCClient, goat_client: &GOATClient, graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, EvmAddress, Vec)], + params_endorse_sigs: &[(PublicKey, EvmAddress, Vec)], ) -> Result<()> { if !graph.operator_pre_signed() || !graph.committee_pre_signed() { bail!(SpecialError::InvalidGraph( @@ -368,6 +537,7 @@ pub mod todo_funcs { // 1) Rebuild full graph to ensure structure is coherent and txns derivable let full_graph = BitvmGcGraph::from_simplified(graph) .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + validate_graph_policy(&graph.parameters)?; verify_graph_operator_pre_signatures(&full_graph).map_err(|e| { SpecialError::InvalidGraph(format!("invalid operator pre-signatures: {e}")) })?; @@ -419,6 +589,13 @@ pub mod todo_funcs { pegin_data.committee_pubkeys.len() ))); } + if params_endorse_sigs.len() != pegin_data.committee_pubkeys.len() { + bail!(SpecialError::InvalidGraph(format!( + "params endorsement count {} does not match instance committee count {}", + params_endorse_sigs.len(), + pegin_data.committee_pubkeys.len() + ))); + } for (pk, evm_addr, sig) in endorse_sigs.iter() { // no duplicates @@ -468,6 +645,55 @@ pub mod todo_funcs { } } + let mut seen_committee: HashSet = HashSet::new(); + let mut seen_evm: HashSet = HashSet::new(); + for (pk, evm_addr, sig) in params_endorse_sigs.iter() { + if !seen_committee.insert(*pk) { + bail!(SpecialError::InvalidGraph( + "duplicate committee pubkey in params endorsements".to_string() + )); + } + if !seen_evm.insert(*evm_addr) { + bail!(SpecialError::InvalidGraph( + "duplicate evm address in params endorsements".to_string() + )); + } + + let mut found = false; + for i in 0..pegin_data.committee_pubkeys.len() { + let on_chain_pk = + PublicKey::from_slice(&pegin_data.committee_pubkeys[i]).map_err(|e| { + SpecialError::InvalidGraph(format!( + "invalid committee pubkey on-chain: {e}" + )) + })?; + if &on_chain_pk == pk { + found = true; + let expected_addr = pegin_data.committee_addresses[i]; + if &expected_addr != evm_addr { + bail!(SpecialError::InvalidGraph( + "committee evm address mismatch in params endorsements".to_string() + )); + } + break; + } + } + if !found { + bail!(SpecialError::InvalidGraph( + "params endorser not in committee set".to_string() + )); + } + + let ok = super::verify_graph_params_endorsement(evm_addr, &full_graph, sig).map_err( + |e| SpecialError::InvalidGraph(format!("failed to verify params endorsement: {e}")), + )?; + if !ok { + bail!(SpecialError::InvalidGraph( + "invalid params endorsement signature".to_string() + )); + } + } + Ok(()) } pub fn prekickoff_replenishment_amount() -> Amount { @@ -2110,15 +2336,23 @@ pub async fn get_watchtower_commitment( } } +#[derive(Debug, PartialEq, Eq)] +pub struct WatchtowerChallengeInfo { + pub challenge_txids: Vec>, + pub included_watchtowers: Vec, + pub resolved_branch_txids: Vec, +} + #[tracing::instrument(name = "get_watchtower_challenge_info", skip(btc_client))] pub async fn get_watchtower_challenge_info( btc_client: &BTCClient, watchtower_challenge_init_txid: &SerializableTxid, watchtower_timeout_txids: &[Txid], num_watchtowers: usize, -) -> Result<(Vec>, Vec)> { +) -> Result { let mut challenge_txids = Vec::with_capacity(num_watchtowers); let mut included_watchtowers = Vec::with_capacity(num_watchtowers); + let mut resolved_branch_txids = Vec::with_capacity(num_watchtowers); for index in 0..num_watchtowers { let challenge_vout = output_topology::watchtower_challenge_init::watchtower_connector(index) as u64; @@ -2131,6 +2365,7 @@ pub async fn get_watchtower_challenge_info( if !status.confirmed { bail!("watchtower branch tx {txid} at index {index} is not confirmed yet"); } + resolved_branch_txids.push(txid); let is_timeout = watchtower_timeout_txids .get(index) @@ -2149,23 +2384,23 @@ pub async fn get_watchtower_challenge_info( } } } - Ok((challenge_txids, included_watchtowers)) + Ok(WatchtowerChallengeInfo { challenge_txids, included_watchtowers, resolved_branch_txids }) } -/// Returns `(btc_best_block_hash, included_watchtowers_bitmap)` from already-fetched -/// `get_watchtower_challenge_info` output. +/// Returns `(btc_best_block_hash, included_watchtowers_bitmap)` using every confirmed +/// challenge-branch resolution for the block hash while only challenges set bitmap bits. pub async fn compute_operator_pubin_blockhash_and_bitmap( btc_client: &BTCClient, - challenge_txids: &[Option], + resolved_branch_txids: &[Txid], included_watchtowers_bits: &[bool], ) -> Result<([u8; 32], [u8; 32])> { let btc_best_block_hash = { let mut largest: Option<(u32, BlockHash)> = None; - for txid in challenge_txids.iter().flatten() { - let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; + for txid in resolved_branch_txids { + let status = btc_client.get_tx_status(txid).await?; let (height, hash) = match (status.block_height, status.block_hash) { (Some(height), Some(hash)) => (height, hash), - _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), + _ => bail!("watchtower branch resolution tx {txid} is not confirmed yet"), }; if largest.is_none_or(|(h, _)| height > h) { largest = Some((height, hash)); @@ -2173,7 +2408,7 @@ pub async fn compute_operator_pubin_blockhash_and_bitmap( } largest .map(|(_, hash)| hash.to_byte_array()) - .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx available"))? + .ok_or_else(|| anyhow!("no confirmed watchtower branch resolution tx available"))? }; let mut included_watchtowers = [0u8; 32]; @@ -2188,13 +2423,13 @@ pub async fn compute_operator_pubin_blockhash_and_bitmap( async fn get_operator_committed_blockhash( btc_client: &BTCClient, - challenge_txids: &[Option], + resolved_branch_txids: &[Txid], included_watchtowers_bits: &[bool], graph_id: Uuid, ) -> Result> { match compute_operator_pubin_blockhash_and_bitmap( btc_client, - challenge_txids, + resolved_branch_txids, included_watchtowers_bits, ) .await @@ -2304,7 +2539,11 @@ pub async fn get_operator_proof( let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); let watchtower_timeout_txids: Vec = bitvm_graph.watchtower_challenge_timeouts.iter().map(|tx| tx.tx().compute_txid()).collect(); - let (watchtower_challenge_txids, included_watchtowers) = match get_watchtower_challenge_info( + let WatchtowerChallengeInfo { + challenge_txids: watchtower_challenge_txids, + included_watchtowers, + resolved_branch_txids, + } = match get_watchtower_challenge_info( btc_client, &watchtower_challenge_init_txid, &watchtower_timeout_txids, @@ -2320,7 +2559,7 @@ pub async fn get_operator_proof( }; let Some(operator_committed_blockhash) = get_operator_committed_blockhash( btc_client, - &watchtower_challenge_txids, + &resolved_branch_txids, &included_watchtowers, graph_id, ) @@ -3309,6 +3548,13 @@ pub async fn endorse_graph(goat_client: &GOATClient, graph: &BitvmGcGraph) -> Re Ok(sig) } +pub async fn endorse_graph_params(graph: &BitvmGcGraph) -> Result { + let signer = PrivateKeySigner::from_str(&get_node_goat_private_key()?)?; + let params_hash = graph.parameters.canonical_graph_params_hash()?; + let sig = signer.sign_hash(¶ms_hash.into()).await?; + Ok(sig) +} + pub async fn endorse_pegin( goat_client: &GOATClient, instance_id: Uuid, @@ -3359,6 +3605,41 @@ pub async fn verify_graph_endorsement( .map_err(|e| e.into()) } +pub fn verify_graph_params_endorsement( + evm_address: &EvmAddress, + graph: &BitvmGcGraph, + signature: &[u8], +) -> Result { + let params_hash = graph.parameters.canonical_graph_params_hash()?; + let sig = EvmSignature::try_from(signature)?; + sig.recover_address_from_prehash(¶ms_hash.into()) + .map(|addr| &addr == evm_address) + .map_err(|e| e.into()) +} + +pub fn sign_verifier_graph_params( + verifier_keypair: Keypair, + graph: &SimplifiedBitvmGcGraph, +) -> Result { + let params_hash = graph.canonical_graph_params_hash()?; + Ok(SECP256K1.sign_schnorr(&SecpMessage::from_digest(params_hash), &verifier_keypair)) +} + +pub fn verify_verifier_graph_params_endorsement( + verifier_pubkey: &PublicKey, + graph: &SimplifiedBitvmGcGraph, + signature: &SchnorrSignature, +) -> Result { + let params_hash = graph.canonical_graph_params_hash()?; + Ok(SECP256K1 + .verify_schnorr( + signature, + &SecpMessage::from_digest(params_hash), + &XOnlyPublicKey::from(*verifier_pubkey), + ) + .is_ok()) +} + /// Validates whether the given kickoff transaction has been confirmed on Layer 1. pub async fn tx_on_chain(client: &BTCClient, txid: &Txid) -> Result { match client.get_tx(txid).await? { @@ -3855,6 +4136,9 @@ pub struct GraphProcessDataItem { pub partial_sigs: Option, pub committee_evm_address: Option, pub endorse_signature: Vec, + pub params_endorse_signature: Vec, + pub verifier_index: Option, + pub verifier_params_signature: Option, } pub type GraphProcessDataMap = IndexMap; @@ -4408,9 +4692,7 @@ pub async fn store_committee_pub_nonces_for_graph( .and_modify(|v| v.committee_pub_nonce = Some(pub_nonces.clone())) .or_insert_with(|| GraphProcessDataItem { committee_pub_nonce: Some(pub_nonces), - partial_sigs: None, - committee_evm_address: None, - endorse_signature: vec![], + ..Default::default() }); upsert_pegin_graph_process_data( &mut storage_processor, @@ -4457,10 +4739,8 @@ pub async fn store_committee_partial_sigs_for_graph( .entry(committee_pubkey) .and_modify(|v| v.partial_sigs = Some(partial_sigs.clone())) .or_insert_with(|| GraphProcessDataItem { - committee_pub_nonce: None, partial_sigs: Some(partial_sigs), - committee_evm_address: None, - endorse_signature: vec![], + ..Default::default() }); upsert_pegin_graph_process_data( &mut storage_processor, @@ -4504,6 +4784,7 @@ pub async fn store_committee_endorsement_for_graph( committee_pubkey: PublicKey, committee_evm_address: EvmAddress, endorse_signature: Vec, + params_endorse_signature: Vec, ) -> Result<()> { let mut storage_processor = local_db.acquire().await?; let (is_endorsed, mut process_data) = @@ -4512,13 +4793,14 @@ pub async fn store_committee_endorsement_for_graph( .entry(committee_pubkey) .and_modify(|v| { v.endorse_signature = endorse_signature.clone(); + v.params_endorse_signature = params_endorse_signature.clone(); v.committee_evm_address = Some(committee_evm_address); }) .or_insert_with(|| GraphProcessDataItem { - committee_pub_nonce: None, - partial_sigs: None, committee_evm_address: Some(committee_evm_address), endorse_signature, + params_endorse_signature, + ..Default::default() }); upsert_pegin_graph_process_data( &mut storage_processor, @@ -4535,6 +4817,7 @@ pub async fn store_committee_endorsements_for_graph( instance_id: Uuid, graph_id: Uuid, endorse_sigs: Vec<(PublicKey, EvmAddress, Vec)>, + params_endorse_sigs: Vec<(PublicKey, EvmAddress, Vec)>, ) -> Result<()> { let mut storage_processor = local_db.acquire().await?; let (is_endorsed, mut process_data) = @@ -4548,10 +4831,22 @@ pub async fn store_committee_endorsements_for_graph( v.committee_evm_address = Some(committee_evm_address); }) .or_insert_with(|| GraphProcessDataItem { - committee_pub_nonce: None, - partial_sigs: None, committee_evm_address: Some(committee_evm_address), endorse_signature, + ..Default::default() + }); + } + for (committee_pubkey, committee_evm_address, params_endorse_signature) in params_endorse_sigs { + process_data + .entry(committee_pubkey) + .and_modify(|v| { + v.params_endorse_signature = params_endorse_signature.clone(); + v.committee_evm_address = Some(committee_evm_address); + }) + .or_insert_with(|| GraphProcessDataItem { + committee_evm_address: Some(committee_evm_address), + params_endorse_signature, + ..Default::default() }); } upsert_pegin_graph_process_data( @@ -4585,6 +4880,94 @@ pub async fn get_committee_endorsements_for_graph( }) .collect::)>>()) } + +pub async fn get_committee_params_endorsements_for_graph( + local_db: &LocalDB, + _instance_id: Uuid, + graph_id: Uuid, +) -> Result)>> { + let mut storage_processor = local_db.acquire().await?; + let (_is_endorsed, process_data) = + find_pegin_graph_process_data(&mut storage_processor, graph_id).await?; + Ok(process_data + .iter() + .filter_map(|(k, v)| { + if !v.params_endorse_signature.is_empty() { + v.committee_evm_address + .as_ref() + .map(|evm_addr| (*k, *evm_addr, v.params_endorse_signature.clone())) + } else { + None + } + }) + .collect::)>>()) +} + +pub async fn store_verifier_graph_params_endorsement( + local_db: &LocalDB, + instance_id: Uuid, + graph_id: Uuid, + verifier_pubkey: PublicKey, + verifier_index: usize, + signature: SchnorrSignature, +) -> Result<()> { + let mut storage_processor = local_db.acquire().await?; + let (is_endorsed, mut process_data) = + find_pegin_graph_process_data(&mut storage_processor, graph_id).await?; + if let Some(existing_index) = process_data.get(&verifier_pubkey).and_then(|v| v.verifier_index) + && existing_index != verifier_index + { + bail!(SpecialError::InvalidGraph(format!( + "verifier params endorsement index changed for graph {graph_id} and verifier {verifier_pubkey}" + ))); + } + if let Some(existing_signature) = + process_data.get(&verifier_pubkey).and_then(|v| v.verifier_params_signature.as_ref()) + && existing_signature != &signature + { + bail!(SpecialError::InvalidGraph(format!( + "verifier params endorsement signature changed for graph {graph_id} and verifier {verifier_pubkey}" + ))); + } + process_data + .entry(verifier_pubkey) + .and_modify(|v| { + v.verifier_index = Some(verifier_index); + v.verifier_params_signature = Some(signature); + }) + .or_insert_with(|| GraphProcessDataItem { + verifier_index: Some(verifier_index), + verifier_params_signature: Some(signature), + ..Default::default() + }); + upsert_pegin_graph_process_data( + &mut storage_processor, + graph_id, + instance_id, + is_endorsed, + &process_data, + ) + .await?; + Ok(()) +} + +pub async fn get_verifier_graph_params_endorsements_for_graph( + local_db: &LocalDB, + _instance_id: Uuid, + graph_id: Uuid, +) -> Result> { + let mut storage_processor = local_db.acquire().await?; + let (_is_endorsed, process_data) = + find_pegin_graph_process_data(&mut storage_processor, graph_id).await?; + Ok(process_data + .iter() + .filter_map(|(k, v)| { + v.verifier_index + .zip(v.verifier_params_signature.as_ref()) + .map(|(index, signature)| (*k, index, signature.clone())) + }) + .collect()) +} pub async fn mark_graph_as_endorsed( local_db: &LocalDB, _instance_id: Uuid, @@ -5374,7 +5757,7 @@ mod commit_pubin_tests { ), ); - let (txids, bits) = get_watchtower_challenge_info( + let info = get_watchtower_challenge_info( &btc_client, &SerializableTxid::from(watchtower_init_txid), &[], @@ -5383,8 +5766,9 @@ mod commit_pubin_tests { .await .unwrap(); - assert_eq!(txids, vec![Some(challenge_txid_wt0.to_string()), None]); - assert_eq!(bits, vec![true, false]); + assert_eq!(info.challenge_txids, vec![Some(challenge_txid_wt0.to_string()), None]); + assert_eq!(info.included_watchtowers, vec![true, false]); + assert_eq!(info.resolved_branch_txids, vec![challenge_txid_wt0]); } #[tokio::test] @@ -5422,12 +5806,11 @@ mod commit_pubin_tests { ), ); - let challenge_txids = - vec![Some(challenge_txid_wt0.to_string()), None, Some(challenge_txid_wt2.to_string())]; + let resolved_branch_txids = vec![challenge_txid_wt0, challenge_txid_wt2]; let bits = vec![true, false, true]; let (best_hash, bitmap) = - compute_operator_pubin_blockhash_and_bitmap(&btc_client, &challenge_txids, &bits) + compute_operator_pubin_blockhash_and_bitmap(&btc_client, &resolved_branch_txids, &bits) .await .unwrap(); From 462664d3ccbcc8d8b07f802100b433ef9cf7c7d2 Mon Sep 17 00:00:00 2001 From: Blake Hau <0xblake.sg@gmail.com> Date: Sat, 11 Jul 2026 13:14:37 +0800 Subject: [PATCH 13/14] Fix: handle empty watchtower challenge txids (#415) --- circuits/operator-proof/host/src/lib.rs | 6 ++++- proof-builder-rpc/src/task/mod.rs | 32 +++++++++++++------------ 2 files changed, 22 insertions(+), 16 deletions(-) diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 9c006875..73231f63 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -94,11 +94,15 @@ fn parse_indexed_watchtower_inputs( watchtower_challenge_txids: &str, watchtower_public_keys: &str, ) -> anyhow::Result<(Vec<(u16, Txid, PublicKey)>, Vec<[u8; 32]>)> { - let txids = watchtower_challenge_txids.split(',').collect::>(); 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" diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index 949c9d49..a6f795f3 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -378,16 +378,13 @@ pub(crate) async fn fetch_on_demand_task( } } - // double check the committed block height is larger than all the watchtower challenge txns'. - match operator_committed_blockhash { - Some(ref hash) => match btc_client.get_block_by_hash(&BlockHash::from_str(hash)?).await { - Ok(Some(block)) => { - if block.bip34_block_height()? != largest_btc_block_height as u64 { - anyhow::bail!( - "Operator committed block {hash} is not confirmed yet, wait for the next round" - ); - } - } + let btc_proof_height = if is_watchtower { + largest_btc_block_height + } else { + let hash = operator_committed_blockhash.as_ref().unwrap(); + let committed_height = match btc_client.get_block_by_hash(&BlockHash::from_str(hash)?).await + { + Ok(Some(block)) => block.bip34_block_height()? as u32, Ok(None) => { tracing::warn!( "Operator committed block hash {hash} is not found in BTC, it might be mempool tx, wait for the next round" @@ -400,13 +397,18 @@ pub(crate) async fn fetch_on_demand_task( ); return Ok(None); } - }, - None => {} // skip for watchtowers + }; + if committed_height < largest_btc_block_height { + anyhow::bail!( + "operator committed block height {committed_height} is lower than challenge tx height {largest_btc_block_height}" + ); + } + committed_height }; let header_chain_input_proof = match storage_processor .find_long_running_task_proof_including_block_number( - largest_btc_block_height as i64, + btc_proof_height as i64, HeaderChainProofBuilder::name(), ) .await? @@ -415,7 +417,7 @@ pub(crate) async fn fetch_on_demand_task( None => { tracing::warn!( "Header chain proof is not ready for block: {}, proof not ready", - largest_btc_block_height + btc_proof_height ); return Ok(None); } @@ -466,7 +468,7 @@ pub(crate) async fn fetch_on_demand_task( // commit chain let commit_chain_input_proof = match storage_processor .find_long_running_task_proof_including_block_number( - largest_btc_block_height as i64, + btc_proof_height as i64, CommitChainProofBuilder::name(), ) .await? From 73cdb49e2a5d845b4953520c92b8085374822223 Mon Sep 17 00:00:00 2001 From: Stephen Duan <81497928+eigmax@users.noreply.github.com> Date: Wed, 22 Jul 2026 09:24:58 +0800 Subject: [PATCH 14/14] audit: use tla+ to formally verify transaction graphs (#417) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: use tla+ to detect the graph update race * fix: use tla+ to detect the graph update race * fix: use tla+ to detect the graph update race * fix: use tla+ to detect the graph update race * fix: use tla+ to detect the graph update race * fix: use tla+ to detect the graph update race * fix: use tla+ to detect the graph update race * fix: use tla+ to detect the graph update race * fix: extract_tla_network_table * fix: extract_tla_network_table * fix: rustfmt crates/bitvm-gc/src/timelocks.rs CI's Rustfmt check was failing on the tla_model_matches_shipped_timelock_configs test added earlier - never ran cargo fmt locally since this crate can't be compiled in the dev sandbox (unrelated zkVM build dependency issue). rustfmt itself doesn't need the crate to build, only to parse, so `cargo fmt --all` applied cleanly. No logic changes. * revert: keep the TLA+ audit, revert the applied code fixes This branch is an audit pass: node/tla/*.tla proves several real races/ boundary bugs exist in the current code, and proves correct fix designs for each - but applying those fixes to Rust is separate work the team should review and land themselves, not something to ship silently inside an audit PR. Reverted to gc-v2 (git checkout gc-v2 -- , clean revert since this branch never touched these files before this session): - node/src/utils.rs (update_graph_status_guarded / set_instance_presigned_guarded atomic-CAS guards) - node/src/scheduled_tasks/event_watch_task.rs (routing through the guard) - node/src/scheduled_tasks/instance_maintenance_tasks.rs (TOCTOU fix) - crates/store/src/localdb.rs (GraphUpdate/InstanceUpdate::only_if_status_in) - crates/store/src/schema.rs (reverse-pointer doc comments) - crates/bitvm-gc/src/timelocks.rs (validate_timelock_config strictness, testnet4 connector_d 34->35) Kept as-is: every node/tla/*.tla and *.cfg file - the formal proofs are the actual audit deliverable and remain accurate regardless of whether the fix is applied yet. Updated to stop claiming the reverted fixes are shipped: - README.md's TLA+ table: "current code" -> "current code (still buggy)" for the bug configs, "proposed fix (verified, not applied)" for the fix configs. Also added the MultiActorRace.tla/.cfg row, built after the table was first written. - node/README.md: "Resolved: ..." section reverted back to "Known gap: ...", now pointing at the verified-but-unapplied fix design instead of claiming it's live. - node/tla/GraphLifecycle.md: same current-code correction. - .github/workflows/ci.yml's tla-plus job: relabeled both steps (baseline+proposed-fix vs known-bug) to match, and added MultiActorRace.cfg to the must-pass set (it didn't exist yet when the job was first written). Verified locally before pushing: all 6 must-pass configs still pass, all 3 known-bug configs still fail as expected, cargo fmt --all -- --check is clean. * fix: clippy lints in babe_adapter.rs Pre-existing on gc-v2, never caught before because CI never ran against gc-v2-based PRs until this branch's pull_request.branches fix. Not related to any change in this audit. - too_many_arguments on verify_real_setup: #[allow], 8 semantically distinct crypto-verification params, not a design smell worth a breaking API refactor. - needless_borrow: &package -> package (package is already &CACSetupPackage). - unnecessary_unwrap: is_none()/unwrap() -> ok_or_else(...), same behavior. * fix: resolve remaining clippy lints in node crate (handle.rs, utils.rs) Pre-existing needless-borrow, clone-on-copy, collapsible-if, and needless-borrows-for-generic-args lints in code never touched by CI clippy until the gc-v2 PR-trigger fix. No behavior change. * audit: extend TLA+ round to InstanceBridgeOutStatus and MessageState races Two new confirmed races found while sweeping every remaining stateful enum in the codebase, each with a verified counterexample and a verified (not applied) fix design: - InstanceBridgeOutStatus: three independently-scheduled writers (RPC stale-upsert, GoatChain event watcher, maintenance monitor) can resurrect a terminal Claim/Timeout/Refund back to Initialize. node/tla/InstanceBridgeOutRace.tla - MessageState: upsert_message's unconditional ON CONFLICT DO UPDATE can resurrect a Cancelled message back to Pending and re-dispatch it after its graph already closed. node/tla/MessageStateRace.tla GoatTxProcessingStatus was also audited and found safe (causal ordering via on-chain proof requirement + a processing gate), with two unrelated lower-priority defects noted for follow-up. Also cross-checked against blake-pro/bitvm-node-formal-verification, an independent Ivy-based effort targeting the same source commit - complementary rather than overlapping coverage (see report). CI's tla-plus job and root README's spec table updated to include the two new specs. audit/TLAPlus-20260630.md is the full narrative report. No Rust behavior changes - audit-only branch. * audit: extend TLA+ round to the Bitcoin transaction graph's connectors Mapped every shared/bottleneck Taproot connector in the goat crate's peg-out transaction graph (not just ConnectorD, the original scope), looking for the same margin-race shape Finding 4 found there. - Finding 9 (new bug): ConnectorA (Take1 vs Challenge) has NO margin check anywhere in validate_timelock_config, unlike every other timelock field. Real shipped Regtest value gives a challenger exactly zero reaction margin - same coin-flip-boundary shape as the original ConnectorD/testnet4 finding. node/tla/Take1ChallengeRace.tla (+ Fixed.cfg design, verified) - Finding 10 (verification, not a bug): ConnectorF leaf 1 has two alternative "committee blocks Take2" spenders (Nack and CommitTimeout), but MultiActorRace.tla only proved the margin for one (Nack). Extended it with operator_commit's margin against ConnectorF using real shipped values - holds on all 4 networks, closing a verification-coverage gap (the Rust ensure_lt check was previously unverified at the TLA+ level). - ConnectorZ and AckConnector's un-covered leaves reviewed and found to have no CSV-margin structure to check (documented reasoning, no forced/artificial model). - Take2DisproveRace.tla's header updated to acknowledge ConnectorD's third leaf (PubinDisprove) - zero CSV, can't be slower than the already-proven-safe Disprove path, so no new model needed, but the header no longer implies it doesn't exist. CI's tla-plus job and root README's spec table updated. No Rust behavior changes - audit-only branch. * audit: refine report — fix stale cross-references and counts Tie Finding 4's connector_a observation forward to Finding 9 (same gap, discovered in two passes) instead of leaving it as a redundant, disconnected aside. Fix two positional counts ("seven"/"three of the seven") that went stale once Finding 9/10 were added after them. * ci: gate fmt/clippy/test behind tla-plus, fail fast on TLA+ failure tla-plus is the fastest job (seconds, no Rust toolchain build) and a failure there means either a real regression or a stale/broken spec - not worth burning 30+ minutes of Cargo Test/Clippy compute before finding out. Reordered it to the top of the file since it now gates everything else via needs:. * audit: dedupe shared TLA+ constants into GraphTopology.tla/ShippedTimelocks.tla Extract the two literal value-table duplications found across node/tla/: - AllStatuses/TerminalStatuses/AllowedTransitions (the real Graph.status state machine) was byte-identical across GraphLifecycle.tla, GraphLifecycleFineGrained.tla, and GraphLifecycleFineGrainedFixed.tla. - Networks/ProverConnector/ConnectorD/ConnectorF/MinReactionBlocks (and more) was byte-identical between Take2DisproveRace.tla and MultiActorRace.tla, with MinReactionBlocks also duplicated a third time in Take1ChallengeRace.tla. Both are now single-source-of-truth modules the relevant specs EXTEND, removing a real drift risk (already caught one: MultiActorRace.tla's comment said "shipped values" without noting ConnectorD is actually the post-fix value, inconsistent with Take2DisproveRace.tla's more careful "(post-fix)" annotation on the identical table). Also removed `EXTENDS FiniteSets` from 5 files that never used Cardinality/IsFiniteSet - dead imports. Net: 1033 -> 1000 lines despite adding 2 new files. Every one of the 15 spec+config combinations re-verified via TLC to behave identically to before (same pass/fail outcome, same state counts - e.g. GraphLifecycleFineGrainedFixed.cfg still finds exactly 132 states, matching the audit report's existing claim). No .cfg files touched; CI's working-directory: node/tla resolves the new EXTENDS targets the same way it already resolves everything else. Also strengthens Finding 8 with direct confirmation against the L2 Gateway.sol contract (KSlashh/bitvm-L2-contracts@2173b92, gc-v2): proceedWithdraw requires a Merkle-proven kickoff tx and is onlyCommittee-gated; finishWithdrawDisproved has its own on-chain AlreadyDisproved guard - confirming this report's races are about the local node's bookkeeping drifting from a well-guarded on-chain source of truth, not the L2 contract itself being unsafe. * ci: make tla-plus job fail while documented bugs remain unfixed Previously the "known-bug specs must still fail" step only failed CI if a bug config unexpectedly started passing (spec/code drift). That meant CI showed green despite 6 TLC-proven, unfixed findings still being present in the shipped code - a clean-looking pipeline hiding real, documented gaps. Now the step fails whenever ANY bug config correctly reproduces its finding (the normal, expected case right now), in addition to the original harder failure if one unexpectedly passes. Since fmt/clippy/ test are gated behind tla-plus, this intentionally keeps the whole pipeline red until each finding's verified fix design is actually applied to the Rust code - moving a bug config to the must-pass list (and its Fixed.cfg out of hiding) is how a fix "turns CI green" again. Verified the exact YAML-parsed script (heredoc dedenting etc.) end to end locally before pushing: correctly identifies all 6 open findings and exits 1. Updated the job's top comment and root README to explain this is expected, not broken. * audit: add fix-implementation guide (write-up only, no code applied) Companion to audit/TLAPlus-20260630.md, per request to generate fix suggestions without reversing this branch's audit-only stance. For each of the 6 CI-gated findings (1/1b, 2, 6, 7, 9) plus Finding 4: exact current code (verified against the real source, not from memory), exact suggested change, and why it implements the property the corresponding TLA+ spec proved. Cross-checked every referenced API against the real codebase before writing it down: - GraphStatus doesn't derive EnumIter, so the Finding 1/2 guard's allow-list is hardcoded from GraphTopology.tla's AllStatuses minus TerminalStatuses instead of a speculative .iter() call. - QueryBuilder::and_where_in already exists (crates/store/src/ utils.rs:77-92) and does exactly what the guard needs - used that instead of hand-rolling placeholder-building SQL. - InstanceUpdate is missing all 5 fields Finding 6b's fix needs (bridge_out_amount/goat_tx_hash/goat_tx_height/user_change_addr/ user_refund_addr) - called that out explicitly rather than waving it away as "add whichever are missing". No Rust source files changed. * ci: generate and print fix-suggestions report in CI instead of a committed doc Remove audit/fix-implementation-guide.md - per feedback, this branch stays audit-only with no extra committed artifacts; the fix guidance belongs in CI output, not a standalone file. The "Fail while documented bugs remain unfixed" step now carries a one-line fix direction + affected files per finding directly in its own script (cfg|tla|finding|files|hint rows) and, for each still-open finding, writes a formatted section to $GITHUB_STEP_SUMMARY (rendered as markdown on the run's summary page) in addition to the existing ::warning:: annotations - so anyone looking at a failed run sees exactly what to fix and where, without needing a separate file. Verified the exact YAML-parsed script end to end locally (including simulating $GITHUB_STEP_SUMMARY) before pushing. * ci: print per-finding fix guidance inline instead of a committed doc Follow-up to 29ef86a, which deleted audit/fix-implementation-guide.md but the actual CI script change didn't get staged in that commit (pathspec typo aborted the git add). This is that missing change. The "Fail while documented bugs remain unfixed" step now carries a one-line fix direction + affected files per finding in its own script (cfg|tla|finding|files|hint rows) and writes a formatted section to $GITHUB_STEP_SUMMARY per still-open finding, in addition to the existing ::warning:: annotations. * ci: print the entire fix-suggestions report, not just one-line hints The condensed per-finding hints weren't enough - print the full write-up (exact current code, exact suggested change, and why it implements the property the TLA+ spec proved, per finding) verbatim to both the raw log and $GITHUB_STEP_SUMMARY whenever any bug remains open. Content is the same detail level as the now-removed audit/fix-implementation-guide.md, just generated inline in the CI script instead of living as a separate committed file. The bug-detection loop is back to simple 3-field records (cfg/tla/ finding) since the fix text is no longer threaded through it - the full report is one static heredoc block printed once, not per-finding interpolation. Verified the exact YAML-parsed script end to end locally: correct exit code, full 298-line report written to both stdout and a simulated GITHUB_STEP_SUMMARY, byte-identical head and tail spot checks against the source content. * Dev fix (#418) * Use JSON for graph params hash * add verifier challenge tool * update log * fix tx-size-too-small * fix instance status transition * fix pegin queue * add sqlx cache * update timelocks * fix graph status transition * fix tla 3,6,7,9 * fix clippy --------- Co-authored-by: ethan * ci: print how to reproduce each open finding, not the fix itself Replace the embedded 295-line fix-suggestions report with generic reproduction instructions: for each open finding, the exact TLC command to reproduce its counterexample locally, plus a pointer to audit/TLAPlus-20260630.md for the verified fix design. The fix content lives in one place (the report) instead of being duplicated in ci.yml where it would drift out of sync. Note: an earlier attempt at this edit used a fragile line-boundary search that matched the wrong closing `fi` and left corrupted duplicate content in the file. Caught before pushing, reset to the last-known-good commit, and redid the edit with exact asserted line boundaries. Verified the final YAML-parsed script end to end locally (correct exit code, clean warnings + step summary, no leftover content) before this commit. * ci: fix broken jar path in printed reproduction commands The repro string printed a bare `tla2tools.jar`, which only resolves if the jar happens to be in the caller's current directory - not where the setup instructions (root README.md) actually put it. Reported by the user copy-pasting a printed command and hitting "Unable to access jarfile". Fixed to the real path, ~/.local/share/tlaplus/tla2tools.jar, matching every other invocation in this workflow and the README. Verified the corrected command actually runs locally before pushing. * audit: update report — all 8 findings fixed and verified in commit 991faaa Every finding this audit formally proved (1, 1b, 2, 3, 4, 6, 7, 9) now has a "What actually shipped" note describing the real applied fix, verified against the actual diff (not the commit message) and, for the two timelock findings, re-checked with TLC using the real new shipped numbers. In several cases the shipped fix goes further than what this audit proposed (per-writer-source transition tables for Finding 1, extra idempotency/collision guards for Finding 6). Finding 8's two adjacent non-race defects were specifically checked and remain unfixed - called out explicitly rather than assumed. node/tla/ShippedTimelocks.tla updated to the real new Testnet4 values (connector_d 35->40, prover_connector 22->20, and others) - re-run through TLC, all properties still hold. Take1ChallengeRace.tla's ConnectorA deliberately left at its historical pre-fix value (a permanent regression-check record); header comments updated to explain this on both files. README.md's spec table reframed: "current code"/"proposed fix" -> "pre-fix code (historical)"/"fix design (applied in 991faaa)". No ci.yml changes - the must-pass/must-fail lists already correctly reflect this state (bug configs still fail against their unchanged historical constants, fix configs still pass against the real current numbers), consistent with CI being designed to keep the bug-reproduction configs as permanent regression checks. * fix: stop tla-plus CI job failing forever on already-fixed historical bugs The "fail while documented bugs remain unfixed" step was designed before commit 991faaa fixed all 8 findings; keeping it failing afterward served no regression-detection purpose (the bug configs' constants are frozen snapshots that never read the real Rust source) while permanently blocking CI. Only an unexpected PASS (a bug config no longer reproducing its counterexample) is a genuine drift signal now, so that's the only case that still fails the job - a config correctly still failing its historical counterexample is now just an informational note in the step summary. * docs: drop the independent Ivy cross-check section from the audit report Keeps the report focused on this audit's own findings; the comparison against the separate blake-pro/bitvm-node-formal-verification project was tangential to the report's purpose. * docs: renamem audit date * add sql cache (#419) Co-authored-by: ethan --------- Co-authored-by: KSlashh <48985735+KSlashh@users.noreply.github.com> Co-authored-by: ethan --- .github/workflows/ci.yml | 102 +- .gitignore | 3 +- Cargo.lock | 13 + README.md | 70 ++ audit/TLAPlus-20260710.md | 224 ++++ circuits/commit-chain-proof/host/src/lib.rs | 23 +- circuits/header-chain-proof/host/src/lib.rs | 23 +- circuits/operator-proof/host/src/lib.rs | 42 +- circuits/state-chain-proof/host/src/lib.rs | 33 +- circuits/watchtower-proof/host/src/lib.rs | 30 +- .../src/signature.rs | 4 +- crates/bitvm-gc/src/babe_adapter.rs | 11 +- crates/bitvm-gc/src/timelocks.rs | 84 +- crates/bitvm-gc/src/types.rs | 2 +- crates/commit-chain/src/commit_chain.rs | 2 +- crates/commit-chain/src/publisher.rs | 33 +- crates/header-chain/src/header_chain.rs | 23 +- crates/state-chain/src/cbft.rs | 4 +- ...0dc8081e776358af4302a88f2f683c8d97ad4.json | 12 + ...260720000000_add_graph_definition_hash.sql | 2 + ...00001_create_graph_compensation_marker.sql | 6 + crates/store/src/localdb.rs | 1081 +++++++++++------ crates/store/src/schema.rs | 158 ++- node/Cargo.toml | 6 +- node/README.md | 84 +- node/src/action.rs | 248 +++- node/src/bin/mock_rpc.rs | 53 +- node/src/bin/send_verifier_challenge.rs | 85 ++ node/src/handle.rs | 808 ++++++++---- node/src/main.rs | 370 ++++-- node/src/metrics_service.rs | 1 + node/src/rpc_service/bitvm.rs | 6 + .../src/rpc_service/handler/bitvm2_handler.rs | 308 +++-- node/src/rpc_service/mod.rs | 156 ++- node/src/rpc_service/routes.rs | 1 + node/src/scheduled_tasks/event_watch_task.rs | 451 +++++-- .../graph_maintenance_tasks.rs | 28 +- .../instance_maintenance_tasks.rs | 115 +- node/src/scheduled_tasks/mod.rs | 242 +++- node/src/utils.rs | 858 +++++++++---- node/tla/GraphLifecycle.cfg | 15 + node/tla/GraphLifecycle.tla | 188 +++ node/tla/GraphLifecycleCoreOnly.cfg | 15 + node/tla/GraphLifecycleFineGrained.cfg | 5 + node/tla/GraphLifecycleFineGrained.tla | 125 ++ node/tla/GraphLifecycleFineGrainedFixed.cfg | 4 + node/tla/GraphLifecycleFineGrainedFixed.tla | 107 ++ node/tla/GraphLifecycleFixed.cfg | 16 + node/tla/GraphTopology.tla | 46 + node/tla/InstanceBridgeOutRace.cfg | 6 + node/tla/InstanceBridgeOutRace.tla | 80 ++ node/tla/InstanceBridgeOutRaceFixed.cfg | 5 + node/tla/InstancePresigned.tla | 76 ++ node/tla/InstancePresignedBug.cfg | 4 + node/tla/InstancePresignedFixed.cfg | 4 + node/tla/MessageStateRace.cfg | 6 + node/tla/MessageStateRace.tla | 78 ++ node/tla/MessageStateRaceFixed.cfg | 5 + node/tla/MultiActorRace.cfg | 6 + node/tla/MultiActorRace.tla | 114 ++ node/tla/ShippedTimelocks.tla | 33 + node/tla/Take1ChallengeRace.cfg | 6 + node/tla/Take1ChallengeRace.tla | 75 ++ node/tla/Take1ChallengeRaceFixed.cfg | 4 + node/tla/Take2DisproveRace.cfg | 4 + node/tla/Take2DisproveRace.tla | 83 ++ proof-builder-rpc/src/api/proof_handler.rs | 4 +- proof-builder-rpc/src/config.rs | 2 +- .../src/task/commit_chain_proof.rs | 2 +- proof-builder-rpc/src/task/mod.rs | 53 +- .../src/task/state_chain_proof.rs | 30 +- 71 files changed, 5392 insertions(+), 1614 deletions(-) create mode 100644 audit/TLAPlus-20260710.md create mode 100644 crates/store/.sqlx/query-ede88a2e872505ac5e58b2e71280dc8081e776358af4302a88f2f683c8d97ad4.json create mode 100644 crates/store/migrations/20260720000000_add_graph_definition_hash.sql create mode 100644 crates/store/migrations/20260720000001_create_graph_compensation_marker.sql create mode 100644 node/src/bin/send_verifier_challenge.rs create mode 100644 node/tla/GraphLifecycle.cfg create mode 100644 node/tla/GraphLifecycle.tla create mode 100644 node/tla/GraphLifecycleCoreOnly.cfg create mode 100644 node/tla/GraphLifecycleFineGrained.cfg create mode 100644 node/tla/GraphLifecycleFineGrained.tla create mode 100644 node/tla/GraphLifecycleFineGrainedFixed.cfg create mode 100644 node/tla/GraphLifecycleFineGrainedFixed.tla create mode 100644 node/tla/GraphLifecycleFixed.cfg create mode 100644 node/tla/GraphTopology.tla create mode 100644 node/tla/InstanceBridgeOutRace.cfg create mode 100644 node/tla/InstanceBridgeOutRace.tla create mode 100644 node/tla/InstanceBridgeOutRaceFixed.cfg create mode 100644 node/tla/InstancePresigned.tla create mode 100644 node/tla/InstancePresignedBug.cfg create mode 100644 node/tla/InstancePresignedFixed.cfg create mode 100644 node/tla/MessageStateRace.cfg create mode 100644 node/tla/MessageStateRace.tla create mode 100644 node/tla/MessageStateRaceFixed.cfg create mode 100644 node/tla/MultiActorRace.cfg create mode 100644 node/tla/MultiActorRace.tla create mode 100644 node/tla/ShippedTimelocks.tla create mode 100644 node/tla/Take1ChallengeRace.cfg create mode 100644 node/tla/Take1ChallengeRace.tla create mode 100644 node/tla/Take1ChallengeRaceFixed.cfg create mode 100644 node/tla/Take2DisproveRace.cfg create mode 100644 node/tla/Take2DisproveRace.tla diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 28495239..65ed43d2 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 @@ -56,6 +155,7 @@ jobs: cargo clippy --all-targets -- -D warnings test: name: Cargo Test + needs: tla-plus runs-on: ubuntu-latest strategy: matrix: @@ -72,5 +172,5 @@ jobs: - name: Run all unit tests run: | set -e - source ~/.zkm-toolchain/env + source ~/.zkm-toolchain/env cargo test -r --all --all-targets diff --git a/.gitignore b/.gitignore index 13c8f4ab..af69b8b7 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 df4b113a..fb7561fb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -13528,6 +13528,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" @@ -13547,12 +13557,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]] diff --git a/README.md b/README.md index d2e442b6..1a696b50 100644 --- a/README.md +++ b/README.md @@ -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/bitvm2-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 00000000..43ac01f1 --- /dev/null +++ b/audit/TLAPlus-20260710.md @@ -0,0 +1,224 @@ +# BitVM2 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/bitvm2-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 BitVM2 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/bitvm2_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/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 400281e8..e5fce407 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -82,14 +82,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)?; @@ -131,8 +131,8 @@ pub async fn fetch_commit_chain( block_height, }; 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) } @@ -144,6 +144,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); @@ -183,7 +184,7 @@ impl ProofBuilder for CommitChainProofBuilder { 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) @@ -193,8 +194,8 @@ impl ProofBuilder for CommitChainProofBuilder { 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(|| { @@ -290,16 +291,16 @@ 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())?; + 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)?; Ok((public_value_hex, proof_size)) } } diff --git a/circuits/header-chain-proof/host/src/lib.rs b/circuits/header-chain-proof/host/src/lib.rs index 9710a5d6..e107db82 100644 --- a/circuits/header-chain-proof/host/src/lib.rs +++ b/circuits/header-chain-proof/host/src/lib.rs @@ -80,8 +80,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 +152,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,9 +199,10 @@ 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) }; @@ -208,7 +211,7 @@ impl ProofBuilder for HeaderChainProofBuilder { 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(|| { @@ -244,7 +247,7 @@ 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, @@ -306,16 +309,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/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 73231f63..5c62a4ae 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -89,11 +89,14 @@ use sha2::{Digest, Sha256}; use std::sync::OnceLock; static ELF_ID: OnceLock = OnceLock::new(); +type IndexedWatchtowerInputs = Vec<(u16, Txid, PublicKey)>; +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<(Vec<(u16, Txid, PublicKey)>, Vec<[u8; 32]>)> { +) -> anyhow::Result<(IndexedWatchtowerInputs, GraphWatchtowerXOnlyPublicKeys)> { let public_keys = watchtower_public_keys .split(',') .map(PublicKey::from_str) @@ -143,9 +146,9 @@ pub async fn fetch_target_block_and_watchtower_tx( )> { let (indexed_watchtower_inputs, graph_watchtower_xonly_public_keys) = parse_indexed_watchtower_inputs(watchtower_challenge_txids, watchtower_public_keys)?; - let btc_client = BTCClient::new(bitcoin_network, Some(&esplora_url)); + 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, @@ -156,7 +159,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 @@ -258,6 +261,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); @@ -316,12 +320,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}'")) @@ -343,12 +347,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}'")) @@ -373,9 +377,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,11 +401,10 @@ impl ProofBuilder for OperatorProofBuilder { // --- 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) @@ -426,7 +428,7 @@ 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, @@ -437,7 +439,7 @@ impl ProofBuilder for OperatorProofBuilder { || -> 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); @@ -495,11 +497,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)?; + 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(&format!("{}", output), proof)?; + std::fs::write(output, proof)?; Ok((public_value_hex, proof_size)) } } diff --git a/circuits/state-chain-proof/host/src/lib.rs b/circuits/state-chain-proof/host/src/lib.rs index e908c95a..b29a6a36 100644 --- a/circuits/state-chain-proof/host/src/lib.rs +++ b/circuits/state-chain-proof/host/src/lib.rs @@ -98,7 +98,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 +150,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 +170,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 +195,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 +208,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 +217,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 +265,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 +276,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,7 +314,7 @@ 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) }; @@ -323,8 +324,8 @@ impl ProofBuilder for StateChainProofBuilder { 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(|| { @@ -407,16 +408,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/watchtower-proof/host/src/lib.rs b/circuits/watchtower-proof/host/src/lib.rs index d7aa2ccc..e0023bc0 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -64,7 +64,7 @@ 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 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 = @@ -88,6 +88,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); @@ -135,12 +136,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}'")) @@ -163,12 +164,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}'")) @@ -193,9 +194,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}'")) @@ -214,10 +214,10 @@ impl ProofBuilder for WatchtowerProofBuilder { } }; // --- 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 latest_sequencer_commit_txid = Txid::from_str(latest_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()) @@ -287,13 +287,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(); - 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!("{}.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)) } } diff --git a/crates/bitcoin-light-client-circuit/src/signature.rs b/crates/bitcoin-light-client-circuit/src/signature.rs index 43f14588..49510880 100644 --- a/crates/bitcoin-light-client-circuit/src/signature.rs +++ b/crates/bitcoin-light-client-circuit/src/signature.rs @@ -127,14 +127,14 @@ 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, diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index 3ee682b6..360c7489 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -206,6 +206,7 @@ pub fn open_real_setup_and_solder( } /// 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, @@ -223,7 +224,7 @@ pub fn verify_real_setup( }; soldering_builder .babe_prover_verify_setup( - &package, + package, &bundle, vk, static_public_inputs, @@ -512,12 +513,8 @@ fn restore_real_verifier( vk: &Groth16VerifyingKey, static_inputs: Fr, ) -> Result { - let verifier = BABEVerifier::from_state(&state.instance_seeds, package, vk, static_inputs); - if verifier.is_none() { - Err(anyhow::anyhow!("Cannot restore real verifier")) - } else { - Ok(verifier.unwrap()) - } + BABEVerifier::from_state(&state.instance_seeds, package, vk, static_inputs) + .ok_or_else(|| anyhow::anyhow!("Cannot restore real verifier")) } fn validate_finalized_indices( diff --git a/crates/bitvm-gc/src/timelocks.rs b/crates/bitvm-gc/src/timelocks.rs index a6570363..835ab487 100644 --- a/crates/bitvm-gc/src/timelocks.rs +++ b/crates/bitvm-gc/src/timelocks.rs @@ -20,12 +20,12 @@ pub const NODE_BITCOIN_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { pub const NODE_TESTNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { connector_z: 100, connector_a: 16, - prover_connector: 22, - connector_d: 34, - watchtower_challenge: 34, - operator_ack: 46, - operator_commit: 58, - connector_f: 70, + prover_connector: 20, + connector_d: 40, + watchtower_challenge: 20, + operator_ack: 32, + operator_commit: 40, + connector_f: 52, }; pub const NODE_SIGNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { connector_z: 6, @@ -39,7 +39,7 @@ pub const NODE_SIGNET_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { }; pub const NODE_REGTEST_TIMELOCK_CONFIG: TimelockConfig = TimelockConfig { connector_z: 1, - connector_a: 1, + connector_a: 2, prover_connector: 1, connector_d: 3, watchtower_challenge: 1, @@ -66,7 +66,20 @@ pub fn estimated_block_interval_secs(network: Network) -> i64 { } } +const MIN_REACTION_SECS: i64 = 3600; + +fn min_reaction_blocks(network: Network) -> u32 { + match network { + Network::Bitcoin | Network::Testnet | Network::Testnet4 => { + let interval = estimated_block_interval_secs(network); + ((MIN_REACTION_SECS + interval - 1) / interval) as u32 + } + Network::Signet | Network::Regtest => 1, + } +} + pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Result<()> { + let min_blocks = min_reaction_blocks(network); for (name, value) in [ ("connector_z", config.connector_z), ("connector_a", config.connector_a), @@ -77,8 +90,11 @@ pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Re ("operator_commit", config.operator_commit), ("connector_f", config.connector_f), ] { - if value == 0 { - bail!("timelock_config.{name} must be greater than 0"); + if value < min_blocks { + bail!( + "timelock_config.{name} must be at least {min_blocks} blocks \ + (~{MIN_REACTION_SECS}s reaction window), got {value}" + ); } } let default_connector_z = default_timelock_config(network).connector_z; @@ -89,7 +105,14 @@ pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Re ); } - ensure_lte("prover_connector", config.prover_connector, "connector_d", config.connector_d)?; + ensure_reaction_margin( + "prover_connector", + config.prover_connector, + "connector_d", + config.connector_d, + min_blocks, + )?; + ensure_gt("connector_a", config.connector_a, "min_reaction_blocks", min_blocks)?; ensure_lt( "watchtower_challenge", config.watchtower_challenge, @@ -102,9 +125,18 @@ pub fn validate_timelock_config(network: Network, config: &TimelockConfig) -> Re Ok(()) } -fn ensure_lte(left_name: &str, left: u32, right_name: &str, right: u32) -> Result<()> { - if left > right { - bail!("timelock_config.{left_name} must be <= timelock_config.{right_name}"); +fn ensure_reaction_margin( + left_name: &str, + left: u32, + right_name: &str, + right: u32, + min_margin: u32, +) -> Result<()> { + if left.saturating_add(min_margin) >= right { + bail!( + "timelock_config.{left_name} must be more than {min_margin} blocks less than \ + timelock_config.{right_name}" + ); } Ok(()) } @@ -116,6 +148,13 @@ fn ensure_lt(left_name: &str, left: u32, right_name: &str, right: u32) -> Result Ok(()) } +fn ensure_gt(left_name: &str, left: u32, right_name: &str, right: u32) -> Result<()> { + if left <= right { + bail!("timelock_config.{left_name} must be > {right_name} ({right})"); + } + Ok(()) +} + pub fn timelock_blocks(_network: Network, blocks: u32) -> u32 { blocks } @@ -155,3 +194,22 @@ pub fn operator_ack_timelock_blocks(network: Network, config: &TimelockConfig) - 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 index abb2c1d5..65e27178 100644 --- a/crates/bitvm-gc/src/types.rs +++ b/crates/bitvm-gc/src/types.rs @@ -718,7 +718,7 @@ impl BitvmGcInstanceParameters { impl BitvmGcGraphParameters { pub fn canonical_graph_params_hash(&self) -> Result<[u8; 32]> { - let encoded = bincode::serialize(self)?; + 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()); diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index bb51246e..4b8c5a08 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -370,7 +370,7 @@ mod tests { fn test_extract_op_return() { // Example: construct a fake tx with OP_RETURN let expected_op_data = [12, 3, 4, 45]; - let script = ScriptBuf::new_op_return(&expected_op_data); + let script = ScriptBuf::new_op_return(expected_op_data); let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, diff --git a/crates/commit-chain/src/publisher.rs b/crates/commit-chain/src/publisher.rs index 193a3d86..5e25fedc 100644 --- a/crates/commit-chain/src/publisher.rs +++ b/crates/commit-chain/src/publisher.rs @@ -219,8 +219,7 @@ mod tests { let prevout = TxOut { value: prev_value, script_pubkey }; // Fake OutPoint - let prev_outpoint = - OutPoint { txid: bitcoin::Txid::from_byte_array([0u8; 32].into()), vout: 0 }; + let prev_outpoint = OutPoint { txid: bitcoin::Txid::from_byte_array([0u8; 32]), vout: 0 }; // === Step 4: construct spending tx === let mut tx = Transaction { @@ -236,7 +235,7 @@ mod tests { value: Amount::from_sat(99_000), script_pubkey: { let btc_pk0 = bitcoin::PublicKey::from(pubkeys[0]); - Address::p2pkh(&btc_pk0, Network::Testnet).script_pubkey() + Address::p2pkh(btc_pk0, Network::Testnet).script_pubkey() }, }], }; @@ -254,15 +253,9 @@ mod tests { finalize(&mut tx, vec![sig1, sig2], &redeem_script).unwrap(); // === Step 7: verify === - let ok = verify_p2wsh_multisig_witness( - &tx, - 0, - &prevout, - &redeem_script, - &pubkeys, - threshold as usize, - ) - .unwrap(); + let ok = + verify_p2wsh_multisig_witness(&tx, 0, &prevout, &redeem_script, &pubkeys, threshold) + .unwrap(); assert!(ok, "2-of-3 multisig witness should verify"); } @@ -278,8 +271,7 @@ mod tests { value: prev_value, script_pubkey: ScriptBuf::new_p2wsh(&ScriptBuf::new().wscript_hash()), }; - let prev_outpoint = - OutPoint { txid: bitcoin::Txid::from_byte_array([0u8; 32].into()), vout: 0 }; + let prev_outpoint = OutPoint { txid: bitcoin::Txid::from_byte_array([0u8; 32]), vout: 0 }; let mut tx = Transaction { version: Version::TWO, @@ -294,7 +286,7 @@ mod tests { value: Amount::from_sat(99_000), script_pubkey: { let btc_pk0 = bitcoin::PublicKey::from(pubkeys[0]); - Address::p2pkh(&btc_pk0, Network::Testnet).script_pubkey() + Address::p2pkh(btc_pk0, Network::Testnet).script_pubkey() }, }], }; @@ -308,15 +300,8 @@ mod tests { finalize(&mut tx, vec![sig1, sig2], &redeem_script).unwrap(); assert!( - verify_p2wsh_multisig_witness( - &tx, - 0, - &prevout, - &redeem_script, - &pubkeys, - threshold as usize, - ) - .is_err() + verify_p2wsh_multisig_witness(&tx, 0, &prevout, &redeem_script, &pubkeys, threshold,) + .is_err() ); } } diff --git a/crates/header-chain/src/header_chain.rs b/crates/header-chain/src/header_chain.rs index 9bfd2a68..a36e9182 100644 --- a/crates/header-chain/src/header_chain.rs +++ b/crates/header-chain/src/header_chain.rs @@ -921,10 +921,10 @@ mod tests { block_headers[..11].iter().map(|header| header.time).collect::>(); // The validation is expected to return false - assert_eq!( - validate_timestamp(block_headers[1].time, first_11_timestamps.try_into().unwrap(),), - false - ); + assert!(!validate_timestamp( + block_headers[1].time, + first_11_timestamps.try_into().unwrap(), + )); } #[test] @@ -937,13 +937,10 @@ mod tests { let first_11_timestamps = block_headers[..11].iter().map(|header| header.time).collect::>(); - assert_eq!( - validate_timestamp( - block_headers[11].time, - first_11_timestamps.clone().try_into().unwrap(), - ), - true - ); + assert!(validate_timestamp( + block_headers[11].time, + first_11_timestamps.clone().try_into().unwrap(), + )); } #[test] @@ -1028,7 +1025,7 @@ mod tests { nonce: 2083236893, }; - let bridge_header: CircuitBlockHeader = header.clone().into(); + let bridge_header: CircuitBlockHeader = header.into(); assert_eq!(bridge_header.version, header.version.to_consensus()); assert_eq!(bridge_header.prev_block_hash, *header.prev_blockhash.as_byte_array()); @@ -1072,7 +1069,7 @@ mod tests { nonce: 2083236893, }; - let bridge_header: CircuitBlockHeader = original_header.clone().into(); + let bridge_header: CircuitBlockHeader = original_header.into(); let converted_header: Header = bridge_header.into(); assert_eq!(original_header, converted_header); diff --git a/crates/state-chain/src/cbft.rs b/crates/state-chain/src/cbft.rs index 36962fd7..6c4a5f9d 100644 --- a/crates/state-chain/src/cbft.rs +++ b/crates/state-chain/src/cbft.rs @@ -153,7 +153,7 @@ mod tests { pub fn test_verify_goat_block() { // https://explorer.goat.network/block/5756298 // curl "http://127.0.0.1:26657/block?height=5756784" | jq .result.block.data - let cosmos_txns: Vec = serde_json::from_str(&LB_1_JSON_TXNS).unwrap(); + let cosmos_txns: Vec = serde_json::from_str(LB_1_JSON_TXNS).unwrap(); let cosmos_txns = cosmos_txns.into_iter().map(|s| BASE64.decode(s).unwrap()).collect::>(); // loght block 5756784 @@ -177,7 +177,7 @@ mod tests { let light_block_2 = serde_json::from_str::(LB_2_JSON).unwrap(); // curl "http://127.0.0.1:26657/block?height=5756785" | jq .result.block.data // https://explorer.goat.network/block/5756299 - let cosmos_txns: Vec = serde_json::from_str(&LB_2_JSON_TXNS).unwrap(); + let cosmos_txns: Vec = serde_json::from_str(LB_2_JSON_TXNS).unwrap(); let cosmos_txns = cosmos_txns.into_iter().map(|s| BASE64.decode(s).unwrap()).collect::>(); check_el_block_from_payload( diff --git a/crates/store/.sqlx/query-ede88a2e872505ac5e58b2e71280dc8081e776358af4302a88f2f683c8d97ad4.json b/crates/store/.sqlx/query-ede88a2e872505ac5e58b2e71280dc8081e776358af4302a88f2f683c8d97ad4.json new file mode 100644 index 00000000..73cd3814 --- /dev/null +++ b/crates/store/.sqlx/query-ede88a2e872505ac5e58b2e71280dc8081e776358af4302a88f2f683c8d97ad4.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "UPDATE instance SET status = ?, status_updated_at = ?, updated_at = ? WHERE instance_id = ? AND status = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 5 + }, + "nullable": [] + }, + "hash": "ede88a2e872505ac5e58b2e71280dc8081e776358af4302a88f2f683c8d97ad4" +} diff --git a/crates/store/migrations/20260720000000_add_graph_definition_hash.sql b/crates/store/migrations/20260720000000_add_graph_definition_hash.sql new file mode 100644 index 00000000..4240e96e --- /dev/null +++ b/crates/store/migrations/20260720000000_add_graph_definition_hash.sql @@ -0,0 +1,2 @@ +ALTER TABLE graph + ADD COLUMN `definition_hash` TEXT NOT NULL DEFAULT ''; diff --git a/crates/store/migrations/20260720000001_create_graph_compensation_marker.sql b/crates/store/migrations/20260720000001_create_graph_compensation_marker.sql new file mode 100644 index 00000000..6f0a7314 --- /dev/null +++ b/crates/store/migrations/20260720000001_create_graph_compensation_marker.sql @@ -0,0 +1,6 @@ +CREATE TABLE graph_compensation_marker ( + graph_id BLOB NOT NULL, + message_id TEXT NOT NULL, + created_at INTEGER NOT NULL, + PRIMARY KEY (graph_id, message_id) +); diff --git a/crates/store/src/localdb.rs b/crates/store/src/localdb.rs index bdd557f9..422dfe34 100644 --- a/crates/store/src/localdb.rs +++ b/crates/store/src/localdb.rs @@ -1,17 +1,19 @@ use crate::utils::{QueryBuilder, QueryParam, create_place_holders}; use crate::{ - BridgeOutGlobalStats, GoatTxRecord, Graph, GraphBtcTxVoutMonitor, GraphRawData, Instance, - LongRunningTaskProof, Message, Node, NodesOverview, OperatorProof, PeginGraphProcessData, - PeginInstanceProcessData, PendingGraphInit, SequencerSetHashChange, SequencerSetScanState, - SerializableTxid, WatchContract, WatchtowerProof, + BridgeOutGlobalStats, GoatTxRecord, Graph, GraphBtcTxVoutMonitor, GraphRawData, GraphStatus, + GraphStatusSource, GraphStatusTransitionOutcome, Instance, LongRunningTaskProof, Message, Node, + NodesOverview, OperatorProof, PeginGraphProcessData, PeginInstanceProcessData, + PendingGraphInit, SequencerSetHashChange, SequencerSetScanState, SerializableTxid, + WatchContract, WatchtowerProof, }; use indexmap::IndexMap; use sqlx::migrate::Migrator; use sqlx::pool::PoolConnection; +use sqlx::sqlite::SqliteRow; use sqlx::types::Uuid; use sqlx::{Row, Sqlite, SqliteConnection, SqlitePool, Transaction, migrate::MigrateDatabase}; -use std::collections::HashMap; +use std::str::FromStr; use std::time::{SystemTime, UNIX_EPOCH}; use tracing::warn; @@ -19,8 +21,20 @@ fn get_current_timestamp_secs() -> i64 { SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() as i64 } -fn get_current_timestamp_millis() -> i64 { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as i64 +fn message_from_row(row: &SqliteRow) -> Result { + Ok(Message { + message_id: row.try_get("message_id")?, + business_id: row.try_get("business_id")?, + actor: row.try_get("actor")?, + from_peer: row.try_get("from_peer")?, + msg_type: row.try_get("msg_type")?, + content: row.try_get("content")?, + state: row.try_get("state")?, + message_version: row.try_get("message_version")?, + weight: row.try_get("weight")?, + lock_time_until: row.try_get("lock_time_until")?, + created_at: row.try_get("created_at")?, + }) } #[derive(Clone, Debug)] @@ -43,13 +57,25 @@ pub struct StorageProcessor<'a> { pub in_transaction: bool, } +#[derive(Clone, Debug, Default)] +pub struct MessageQueueStats { + pub pending_ready: i64, + pub pending_locked: i64, + pub failed: i64, + pub oldest_pending_at: Option, +} + static MIGRATOR: Migrator = sqlx::migrate!("./migrations"); impl LocalDB { pub async fn new(path: &str, is_mem: bool) -> LocalDB { if !Sqlite::database_exists(path).await.unwrap_or(false) { tracing::info!("Creating database {}", path); match Sqlite::create_database(path).await { - Ok(_) => println!("Create db success"), + Ok(_) => tracing::info!( + event = "database_lifecycle", + outcome = "created", + "local database created" + ), Err(error) => panic!("error: {error}"), } } else { @@ -81,6 +107,16 @@ impl LocalDB { in_transaction: true, }) } + + /// Start a short write transaction before reading state that will be + /// immediately reconciled. This prevents a stale snapshot from dropping + /// a concurrent update between the read and the conditional write. + pub async fn start_immediate_transaction<'a>(&self) -> anyhow::Result> { + Ok(StorageProcessor { + conn: ConnectionHolder::Transaction(self.conn.begin_with("BEGIN IMMEDIATE").await?), + in_transaction: true, + }) + } } #[derive(Clone, Debug, Default)] @@ -211,44 +247,54 @@ pub struct InstanceUpdate { pub to_addr: Option, pub btc_txid: Option, pub status: Option, - pub pegin_confirm_txid: Option, + pub pegin_confirm_txid: Option, + pub pegin_cancel_txid: Option, pub post_pegin_txhash: Option, pub btc_height: Option, - pub committees_answers: Option>>, + pub committees_answers: Option>>, pub bridge_out_lock_time: Option, + pub bridge_out_amount: Option, + pub goat_tx_hash: Option, + pub goat_tx_height: Option, + pub user_change_addr: Option, + pub user_refund_addr: Option, + pub only_if_status_in: Option>, + pub only_if_is_bridge_in: Option, + pub only_if_goat_tx_hash: Option, } impl InstanceUpdate { - /// Create new update parameters - pub fn new_with_instance_id(instance_id: Uuid) -> Self { + fn empty() -> Self { Self { - instance_id: Some(instance_id), + instance_id: None, escrow_hash: None, from_addr: None, to_addr: None, btc_txid: None, status: None, pegin_confirm_txid: None, + pegin_cancel_txid: None, post_pegin_txhash: None, btc_height: None, committees_answers: None, bridge_out_lock_time: None, + bridge_out_amount: None, + goat_tx_hash: None, + goat_tx_height: None, + user_change_addr: None, + user_refund_addr: None, + only_if_status_in: None, + only_if_is_bridge_in: None, + only_if_goat_tx_hash: None, } } + + /// Create new update parameters + pub fn new_with_instance_id(instance_id: Uuid) -> Self { + Self { instance_id: Some(instance_id), ..Self::empty() } + } pub fn new_with_escrow_hash(escrow_hash: String) -> Self { - Self { - instance_id: None, - escrow_hash: Some(escrow_hash), - to_addr: None, - from_addr: None, - btc_txid: None, - status: None, - pegin_confirm_txid: None, - post_pegin_txhash: None, - btc_height: None, - committees_answers: None, - bridge_out_lock_time: None, - } + Self { escrow_hash: Some(escrow_hash), ..Self::empty() } } /// Set from_addr @@ -274,12 +320,18 @@ impl InstanceUpdate { self } - /// Set pegin confirmation information - pub fn with_pegin_confirm(mut self, txid: String, _fee: i64) -> Self { + /// Set pegin confirmation transaction ID. + pub fn with_pegin_confirm_txid(mut self, txid: SerializableTxid) -> Self { self.pegin_confirm_txid = Some(txid); self } + /// Set pegin cancellation transaction ID. + pub fn with_pegin_cancel_txid(mut self, txid: SerializableTxid) -> Self { + self.pegin_cancel_txid = Some(txid); + self + } + /// Set post pegin information pub fn with_post_pegin(mut self, txid: String) -> Self { self.post_pegin_txhash = Some(txid); @@ -287,7 +339,10 @@ impl InstanceUpdate { } /// Set committees answers - pub fn with_committees_answers(mut self, committees_answers: HashMap>) -> Self { + pub fn with_committees_answers( + mut self, + committees_answers: IndexMap>, + ) -> Self { self.committees_answers = Some(committees_answers); self } @@ -303,6 +358,52 @@ impl InstanceUpdate { self.bridge_out_lock_time = Some(bridge_out_lock_time); self } + + pub fn with_bridge_out_amount(mut self, bridge_out_amount: String) -> Self { + self.bridge_out_amount = Some(bridge_out_amount); + self + } + + pub fn with_goat_tx_hash(mut self, goat_tx_hash: String) -> Self { + self.goat_tx_hash = Some(goat_tx_hash); + self + } + + pub fn with_goat_tx_height(mut self, goat_tx_height: i64) -> Self { + self.goat_tx_height = Some(goat_tx_height); + self + } + + pub fn with_user_change_addr(mut self, user_change_addr: String) -> Self { + self.user_change_addr = Some(user_change_addr); + self + } + + pub fn with_user_refund_addr(mut self, user_refund_addr: String) -> Self { + self.user_refund_addr = Some(user_refund_addr); + self + } + + /// Apply this update only while the instance is still in one of the + /// expected states. The condition is folded into the UPDATE statement. + pub fn with_only_if_status_in(mut self, statuses: Vec) -> Self { + self.only_if_status_in = Some(statuses); + self + } + + /// Apply this update only to the expected bridge direction. + pub fn with_only_if_is_bridge_in(mut self, is_bridge_in: bool) -> Self { + self.only_if_is_bridge_in = Some(is_bridge_in); + self + } + + /// Apply this update only when the existing Goat transaction hash is the + /// expected value. Used to make swap initialization idempotent. + pub fn with_only_if_goat_tx_hash(mut self, goat_tx_hash: String) -> Self { + self.only_if_goat_tx_hash = Some(goat_tx_hash); + self + } + /// Check if any fields need to be updated pub fn has_updates(&self) -> bool { self.escrow_hash.is_some() @@ -311,10 +412,16 @@ impl InstanceUpdate { || self.btc_txid.is_some() || self.status.is_some() || self.pegin_confirm_txid.is_some() + || self.pegin_cancel_txid.is_some() || self.post_pegin_txhash.is_some() || self.btc_height.is_some() || self.committees_answers.is_some() || self.bridge_out_lock_time.is_some() + || self.bridge_out_amount.is_some() + || self.goat_tx_hash.is_some() + || self.goat_tx_height.is_some() + || self.user_change_addr.is_some() + || self.user_refund_addr.is_some() } pub fn get_query_builder(&self, base_sql: &str) -> QueryBuilder { @@ -327,7 +434,11 @@ impl InstanceUpdate { } if let Some(ref txid) = self.pegin_confirm_txid { - query_builder.set_field("pegin_confirm_txid", QueryParam::Text(txid.clone())); + query_builder.set_field("pegin_confirm_txid", QueryParam::BTCTxid(txid.clone())); + } + + if let Some(ref txid) = self.pegin_cancel_txid { + query_builder.set_field("pegin_cancel_txid", QueryParam::BTCTxid(txid.clone())); } if let Some(ref txid) = self.post_pegin_txhash { @@ -354,6 +465,33 @@ impl InstanceUpdate { query_builder.set_field("bridge_out_lock_time", QueryParam::Int(bridge_out_lock_time)); } + if let Some(ref bridge_out_amount) = self.bridge_out_amount { + query_builder + .set_field("bridge_out_amount", QueryParam::Text(bridge_out_amount.clone())); + } + + if let Some(ref goat_tx_hash) = self.goat_tx_hash { + query_builder.set_field("goat_tx_hash", QueryParam::Text(goat_tx_hash.clone())); + } + + if let Some(goat_tx_height) = self.goat_tx_height { + query_builder.set_field("goat_tx_height", QueryParam::Int(goat_tx_height)); + } + + if let Some(ref user_change_addr) = self.user_change_addr { + query_builder.set_field("user_change_addr", QueryParam::Text(user_change_addr.clone())); + } + + if let Some(ref user_refund_addr) = self.user_refund_addr { + query_builder.set_field("user_refund_addr", QueryParam::Text(user_refund_addr.clone())); + } + + if let Some(ref committees_answers) = self.committees_answers { + let committees_answers = serde_json::to_string(committees_answers) + .expect("IndexMap> serialization is infallible"); + query_builder.set_field("committees_answers", QueryParam::Text(committees_answers)); + } + // Add update time let current_time = get_current_timestamp_secs(); query_builder.set_field("updated_at", QueryParam::Int(current_time)); @@ -370,6 +508,25 @@ impl InstanceUpdate { .and_where("escrow_hash = ? ", Some(QueryParam::Text(escrow_hash.clone()))); } + if let Some(ref statuses) = self.only_if_status_in { + if statuses.is_empty() { + // An empty allow-list must reject the update rather than + // silently dropping the compare-and-swap guard. + query_builder.and_where("1 = 0", None); + } else { + query_builder.and_where_in("status", statuses, false); + } + } + + if let Some(is_bridge_in) = self.only_if_is_bridge_in { + query_builder.and_where("is_bridge_in = ?", Some(QueryParam::Bool(is_bridge_in))); + } + + if let Some(ref goat_tx_hash) = self.only_if_goat_tx_hash { + query_builder + .and_where("goat_tx_hash = ?", Some(QueryParam::Text(goat_tx_hash.clone()))); + } + query_builder } } @@ -497,89 +654,40 @@ impl GraphQuery { } } +/// Runtime graph fields that are not part of the signed graph definition. +/// +/// Status is intentionally absent. All graph status changes must go through +/// `StorageProcessor::transition_graph_status` so a stale event cannot replace +/// a later chain-observed state. #[derive(Clone, Debug)] -pub struct GraphUpdate { +pub struct GraphRuntimeUpdate { + pub instance_id: Uuid, pub graph_id: Uuid, - pub status: Option, - pub sub_status: Option, pub challenge_txid: Option, - pub disprove_txids: Option>, - pub watchtower_challenge_timeout_txids: Option>, - pub operator_challenge_nack_txids: Option>, - pub operator_commit_timeout_txid: Option>, pub bridge_out_start_at: Option, pub init_withdraw_tx_hash: Option, pub proceed_withdraw_height: Option, } -impl GraphUpdate { +impl GraphRuntimeUpdate { /// Create new update parameters - pub fn new(graph_id: Uuid) -> Self { + pub fn new(instance_id: Uuid, graph_id: Uuid) -> Self { Self { + instance_id, graph_id, - status: None, - sub_status: None, challenge_txid: None, - disprove_txids: None, - watchtower_challenge_timeout_txids: None, - operator_challenge_nack_txids: None, - operator_commit_timeout_txid: None, bridge_out_start_at: None, init_withdraw_tx_hash: None, proceed_withdraw_height: None, } } - /// Set status - pub fn with_status(mut self, status: String) -> Self { - self.status = Some(status); - self - } - /// Set sub_status - pub fn with_sub_status(mut self, sub_status: String) -> Self { - self.sub_status = Some(sub_status); - self - } - /// Set challenge transaction ID pub fn with_challenge_txid(mut self, challenge_txid: SerializableTxid) -> Self { self.challenge_txid = Some(challenge_txid); self } - /// Set disprove transaction IDs - pub fn with_disprove_txids(mut self, disprove_txids: Vec) -> Self { - self.disprove_txids = Some(disprove_txids); - self - } - - /// Set watchtower challenge timeout transaction IDs - pub fn with_watchtower_challenge_timeout_txids( - mut self, - watchtower_challenge_timeout_txids: Vec, - ) -> Self { - self.watchtower_challenge_timeout_txids = Some(watchtower_challenge_timeout_txids); - self - } - - /// Set operator challenge NACK transaction IDs - pub fn with_operator_challenge_nack_txids( - mut self, - operator_challenge_nack_txids: Vec, - ) -> Self { - self.operator_challenge_nack_txids = Some(operator_challenge_nack_txids); - self - } - - /// Set operator commit timeout transaction ID - pub fn with_operator_commit_timeout_txid( - mut self, - operator_commit_timeout_txid: Option, - ) -> Self { - self.operator_commit_timeout_txid = Some(operator_commit_timeout_txid); - self - } - /// Set bridge out start time pub fn with_bridge_out_start_at(mut self, bridge_out_start_at: i64) -> Self { self.bridge_out_start_at = Some(bridge_out_start_at); @@ -600,13 +708,7 @@ impl GraphUpdate { /// Check if any fields need to be updated pub fn has_updates(&self) -> bool { - self.status.is_some() - || self.sub_status.is_some() - || self.challenge_txid.is_some() - || self.disprove_txids.is_some() - || self.watchtower_challenge_timeout_txids.is_some() - || self.operator_challenge_nack_txids.is_some() - || self.operator_commit_timeout_txid.is_some() + self.challenge_txid.is_some() || self.bridge_out_start_at.is_some() || self.init_withdraw_tx_hash.is_some() || self.proceed_withdraw_height.is_some() @@ -615,51 +717,9 @@ impl GraphUpdate { pub fn get_query_builder(&self, base_sql: &str) -> QueryBuilder { let mut query_builder = QueryBuilder::update(base_sql); // Add SET fields - if let Some(ref status) = self.status { - query_builder.set_field("status", QueryParam::Text(status.clone())); - query_builder - .set_field("status_updated_at", QueryParam::Int(get_current_timestamp_secs())); - } - if let Some(ref sub_status) = self.sub_status { - query_builder.set_field("sub_status", QueryParam::Text(sub_status.clone())); - } if let Some(ref challenge_txid) = self.challenge_txid { query_builder.set_field("challenge_txid", QueryParam::BTCTxid(challenge_txid.clone())); } - if let Some(ref disprove_txids) = self.disprove_txids { - let disprove_txids_json = - serde_json::to_string(disprove_txids).unwrap_or_else(|_| "[]".into()); - query_builder.set_field("disprove_txids", QueryParam::Text(disprove_txids_json)); - } - if let Some(ref watchtower_challenge_timeout_txids) = - self.watchtower_challenge_timeout_txids - { - let watchtower_challenge_timeout_txids_json = - serde_json::to_string(watchtower_challenge_timeout_txids) - .unwrap_or_else(|_| "[]".into()); - query_builder.set_field( - "watchtower_challenge_timeout_txids", - QueryParam::Text(watchtower_challenge_timeout_txids_json), - ); - } - if let Some(ref operator_challenge_nack_txids) = self.operator_challenge_nack_txids { - let operator_challenge_nack_txids_json = - serde_json::to_string(operator_challenge_nack_txids) - .unwrap_or_else(|_| "[]".into()); - query_builder.set_field( - "operator_challenge_nack_txids", - QueryParam::Text(operator_challenge_nack_txids_json), - ); - } - if let Some(ref operator_commit_timeout_txid) = self.operator_commit_timeout_txid { - match operator_commit_timeout_txid { - Some(operator_commit_timeout_txid) => query_builder.set_field( - "operator_commit_timeout_txid", - QueryParam::BTCTxid(operator_commit_timeout_txid.clone()), - ), - None => query_builder.set_field_null("operator_commit_timeout_txid"), - } - } if let Some(bridge_out_start_at) = self.bridge_out_start_at { query_builder.set_field("bridge_out_start_at", QueryParam::Int(bridge_out_start_at)); } @@ -687,6 +747,10 @@ impl GraphUpdate { "hex(graph_id) = ? COLLATE NOCASE", Some(QueryParam::Text(hex::encode(self.graph_id))), ); + query_builder.and_where( + "hex(instance_id) = ? COLLATE NOCASE", + Some(QueryParam::Text(hex::encode(self.instance_id))), + ); query_builder } @@ -845,6 +909,56 @@ impl<'a> StorageProcessor<'a> { Ok(res.rows_affected() > 0) } + /// Insert an instance only when its ID is not already present. + /// + /// Creation paths that race with status transitions must use this instead + /// of `upsert_instance`, whose `INSERT OR REPLACE` semantics can restore + /// a stale full row over a newer terminal status. + pub async fn insert_instance_if_absent(&mut self, instance: &Instance) -> anyhow::Result { + let committees_answers_json = serde_json::to_string(&instance.committees_answers)?; + let res = sqlx::query( + "INSERT INTO instance \ + (instance_id, is_bridge_in, network, from_addr, to_addr, amount, fees, input_utxos, \ + status, goat_tx_hash, goat_tx_height, user_xonly_pubkey, user_change_addr, \ + user_refund_addr, btc_txid, pegin_confirm_txid, pegin_cancel_txid, committees_answers, \ + pegin_data_tx_hash, btc_height, parameters, status_updated_at, escrow_hash, \ + bridge_out_lock_time, post_pegin_txhash, bridge_out_amount, created_at, updated_at) \ + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) \ + ON CONFLICT(instance_id) DO NOTHING", + ) + .bind(instance.instance_id) + .bind(instance.is_bridge_in) + .bind(&instance.network) + .bind(&instance.from_addr) + .bind(&instance.to_addr) + .bind(instance.amount) + .bind(instance.fees) + .bind(&instance.input_utxos) + .bind(&instance.status) + .bind(&instance.goat_tx_hash) + .bind(instance.goat_tx_height) + .bind(instance.user_xonly_pubkey) + .bind(&instance.user_change_addr) + .bind(&instance.user_refund_addr) + .bind(&instance.btc_txid) + .bind(&instance.pegin_confirm_txid) + .bind(&instance.pegin_cancel_txid) + .bind(committees_answers_json) + .bind(&instance.pegin_data_tx_hash) + .bind(instance.btc_height) + .bind(&instance.parameters) + .bind(instance.status_updated_at) + .bind(&instance.escrow_hash) + .bind(instance.bridge_out_lock_time) + .bind(&instance.post_pegin_txhash) + .bind(&instance.bridge_out_amount) + .bind(instance.created_at) + .bind(instance.updated_at) + .execute(self.conn()) + .await?; + Ok(res.rows_affected() > 0) + } + /// Find a single instance by its ID /// /// Retrieves an instance from the database using its unique instance_id. @@ -976,6 +1090,29 @@ impl<'a> StorageProcessor<'a> { Ok(result.rows_affected() > 0) } + /// Transition an instance only when it is still in the expected status. + pub async fn update_instance_status_if_current( + &mut self, + instance_id: &Uuid, + current_status: &str, + new_status: &str, + ) -> anyhow::Result { + let current_time = get_current_timestamp_secs(); + let result = sqlx::query!( + "UPDATE instance SET status = ?, status_updated_at = ?, updated_at = ? \ + WHERE instance_id = ? AND status = ?", + new_status, + current_time, + current_time, + instance_id, + current_status, + ) + .execute(self.conn()) + .await?; + + Ok(result.rows_affected() > 0) + } + /// Update instance pegin confirmation information /// /// Method specifically for updating pegin confirmation transaction ID and fee @@ -1026,6 +1163,9 @@ impl<'a> StorageProcessor<'a> { if !params.has_updates() { return Ok(false); } + if params.instance_id.is_none() && params.escrow_hash.is_none() { + anyhow::bail!("instance update requires instance_id or escrow_hash"); + } let query_builder = params.get_query_builder("instance"); // Get SQL and parameters let update_sql = query_builder.get_sql(); @@ -1038,28 +1178,28 @@ impl<'a> StorageProcessor<'a> { /// Add or update a single committee answer for an instance /// - /// This method allows adding or updating a single committee's answer - /// without needing to provide the entire committees_answers HashMap. + /// This method merges one committee answer atomically so concurrent event + /// handlers cannot overwrite each other's answers with stale full maps. pub async fn update_instance_committee_answer( &mut self, instance_id: &Uuid, committee_addr: &str, pubkey: Vec, ) -> anyhow::Result { - // First, get the current committees_answers - let current_instance = self.find_instance(instance_id).await?; - if current_instance.is_none() { - return Ok(false); - } - - let mut committees_answers = current_instance.unwrap().committees_answers; - committees_answers - .entry(committee_addr.to_string()) - .and_modify(|existing| { - *existing = pubkey.clone(); - }) - .or_insert_with(|| pubkey); - self.update_instance_committees_answers_map(instance_id, &committees_answers).await + let committee_patch = serde_json::json!({ (committee_addr): pubkey }).to_string(); + let current_time = get_current_timestamp_secs(); + let result = sqlx::query( + "UPDATE instance \ + SET committees_answers = json_patch(COALESCE(committees_answers, '{}'), json(?)), \ + updated_at = ? \ + WHERE instance_id = ?", + ) + .bind(committee_patch) + .bind(current_time) + .bind(instance_id) + .execute(self.conn()) + .await?; + Ok(result.rows_affected() > 0) } /// Remove a committee answer from an instance @@ -1070,16 +1210,23 @@ impl<'a> StorageProcessor<'a> { instance_id: &Uuid, committee: &str, ) -> anyhow::Result { - // First, get the current committees_answers - let current_instance = self.find_instance(instance_id).await?; - if current_instance.is_none() { - return Ok(false); - } - let mut committees_answers = current_instance.unwrap().committees_answers; - // Remove the committee answer - committees_answers.shift_remove(committee); - // Update the instance with the new committees_answers - self.update_instance_committees_answers_map(instance_id, &committees_answers).await + // JSON merge-patch removes object members with a null value, so this + // stays atomic with concurrent single-answer additions. + let committee_patch = + serde_json::json!({ (committee): serde_json::Value::Null }).to_string(); + let current_time = get_current_timestamp_secs(); + let result = sqlx::query( + "UPDATE instance \ + SET committees_answers = json_patch(COALESCE(committees_answers, '{}'), json(?)), \ + updated_at = ? \ + WHERE instance_id = ?", + ) + .bind(committee_patch) + .bind(current_time) + .bind(instance_id) + .execute(self.conn()) + .await?; + Ok(result.rows_affected() > 0) } /// Get committees answers for an instance @@ -1097,10 +1244,10 @@ impl<'a> StorageProcessor<'a> { } } - /// Update instance committees answers with HashMap (convenience method) + /// Replace the complete committee-answer map. /// - /// This is a convenience method that accepts a HashMap directly. - /// Internally converts it to arrays and calls the main update method. + /// Callers that add a single answer should use + /// `update_instance_committee_answer` instead, which merges atomically. pub async fn update_instance_committees_answers_map( &mut self, instance_id: &Uuid, @@ -1219,81 +1366,238 @@ impl<'a> StorageProcessor<'a> { Ok(result.rows_affected()) } - /// Insert or update a graph - /// - /// Performs an INSERT OR REPLACE operation on the graph table. - /// If a graph with the same graph_id exists, it will be updated. - /// If no graph exists, a new one will be created. + /// Insert a graph definition or verify a compatible replay. /// - /// Parameters: - /// - graph: The complete graph data to insert or update - /// - /// Returns: - /// - Ok(affected_rows) number of rows affected by the operation - /// - Err if the operation failed - pub async fn upsert_graph(&mut self, graph: &Graph) -> anyhow::Result { + /// The canonical definition hash is an identity fence: a new graph may + /// never reuse an existing graph id and inherit its runtime projection. + /// Compatible replays may only fill missing non-identity metadata; they + /// never replace status, observed transaction ids, or withdraw metadata. + pub async fn upsert_graph_definition(&mut self, graph: &Graph) -> anyhow::Result { + if graph.definition_hash.is_empty() { + anyhow::bail!("graph {} is missing its definition hash", graph.graph_id); + } + if graph.status != GraphStatus::OperatorPresigned.to_string() + || !graph.sub_status.is_empty() + || graph.challenge_txid.is_some() + || graph.init_withdraw_tx_hash.is_some() + || graph.bridge_out_start_at != 0 + || graph.proceed_withdraw_height != 0 + { + anyhow::bail!( + "graph {} definition writes must use the OperatorPresigned baseline without runtime data", + graph.graph_id + ); + } let verifier_assert_txids_json = serde_json::to_string(&graph.verifier_assert_txids)?; let disprove_txids_json = serde_json::to_string(&graph.disprove_txids)?; let watchtower_challenge_timeout_txids_json = serde_json::to_string(&graph.watchtower_challenge_timeout_txids)?; let operator_challenge_nack_txids_json = serde_json::to_string(&graph.operator_challenge_nack_txids)?; - let res = sqlx::query!( - "INSERT OR - REPLACE INTO graph (graph_id, instance_id, kickoff_index, from_addr, to_addr, amount, challenge_amount, - status, sub_status, operator_pubkey, cur_prekickoff_txid, next_prekickoff, force_skip_kickoff_txid, + let res = sqlx::query( + "INSERT INTO graph (graph_id, instance_id, kickoff_index, from_addr, to_addr, amount, challenge_amount, + status, sub_status, operator_pubkey, definition_hash, cur_prekickoff_txid, next_prekickoff, force_skip_kickoff_txid, quick_challenge_txid, challenge_incomplete_kickoff_txid, pegin_txid, kickoff_txid, take1_txid, challenge_txid, take2_txid, watchtower_challenge_init_txid, operator_assert_txid, verifier_assert_txids, disprove_txids, watchtower_challenge_timeout_txids, operator_challenge_nack_txids, operator_commit_timeout_txid, - init_withdraw_tx_hash, - bridge_out_start_at, status_updated_at, proceed_withdraw_height, created_at, updated_at) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", - graph.graph_id, - graph.instance_id, - graph.kickoff_index, - graph.from_addr, - graph.to_addr, - graph.amount, - graph.challenge_amount, - graph.status, - graph.sub_status, - graph.operator_pubkey, - graph.cur_prekickoff_txid, - graph.next_prekickoff, - graph.force_skip_kickoff_txid, - graph.quick_challenge_txid, - graph.challenge_incomplete_kickoff_txid, - graph.pegin_txid, - graph.kickoff_txid, - graph.take1_txid, - graph.challenge_txid, - graph.take2_txid, - graph.watchtower_challenge_init_txid, - graph.operator_assert_txid, - verifier_assert_txids_json, - disprove_txids_json, - watchtower_challenge_timeout_txids_json, - operator_challenge_nack_txids_json, - graph.operator_commit_timeout_txid, - graph.init_withdraw_tx_hash, - graph.bridge_out_start_at, - graph.status_updated_at, - graph.proceed_withdraw_height, - graph.created_at, - graph.updated_at, - ) + init_withdraw_tx_hash, bridge_out_start_at, status_updated_at, proceed_withdraw_height, created_at, updated_at) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON CONFLICT(graph_id) DO UPDATE SET + from_addr = CASE + WHEN graph.from_addr = '' AND excluded.from_addr <> '' THEN excluded.from_addr + ELSE graph.from_addr + END, + to_addr = CASE + WHEN graph.to_addr = '' AND excluded.to_addr <> '' THEN excluded.to_addr + ELSE graph.to_addr + END + WHERE graph.definition_hash = excluded.definition_hash + AND graph.instance_id = excluded.instance_id", + ) + .bind(graph.graph_id) + .bind(graph.instance_id) + .bind(graph.kickoff_index) + .bind(&graph.from_addr) + .bind(&graph.to_addr) + .bind(graph.amount) + .bind(graph.challenge_amount) + .bind(GraphStatus::OperatorPresigned.to_string()) + .bind("") + .bind(&graph.operator_pubkey) + .bind(&graph.definition_hash) + .bind(graph.cur_prekickoff_txid.clone()) + .bind(graph.next_prekickoff.clone()) + .bind(graph.force_skip_kickoff_txid.clone()) + .bind(graph.quick_challenge_txid.clone()) + .bind(graph.challenge_incomplete_kickoff_txid.clone()) + .bind(graph.pegin_txid.clone()) + .bind(graph.kickoff_txid.clone()) + .bind(graph.take1_txid.clone()) + .bind(Option::::None) + .bind(graph.take2_txid.clone()) + .bind(graph.watchtower_challenge_init_txid.clone()) + .bind(graph.operator_assert_txid.clone()) + .bind(verifier_assert_txids_json) + .bind(disprove_txids_json) + .bind(watchtower_challenge_timeout_txids_json) + .bind(operator_challenge_nack_txids_json) + .bind(graph.operator_commit_timeout_txid.clone()) + .bind(Option::::None) + .bind(0_i64) + .bind(graph.status_updated_at) + .bind(0_i64) + .bind(graph.created_at) + .bind(graph.updated_at) .execute(self.conn()) .await?; + if res.rows_affected() == 0 { + let Some(existing) = self.find_graph(&graph.graph_id).await? else { + anyhow::bail!("graph {} disappeared while storing its definition", graph.graph_id); + }; + if existing.definition_hash != graph.definition_hash { + anyhow::bail!( + "conflicting graph definition for graph_id {}: existing={}, incoming={}", + graph.graph_id, + existing.definition_hash, + graph.definition_hash + ); + } + if existing.instance_id != graph.instance_id { + anyhow::bail!( + "graph definition instance mismatch for graph_id {}: existing={}, incoming={}", + graph.graph_id, + existing.instance_id, + graph.instance_id + ); + } + } Ok(res.rows_affected()) } - pub async fn update_graph(&mut self, params: &GraphUpdate) -> anyhow::Result<()> { + pub async fn update_graph_runtime( + &mut self, + params: &GraphRuntimeUpdate, + ) -> anyhow::Result { + if !params.has_updates() { + return Ok(false); + } let query_builder = params.get_query_builder("graph"); let update_sql = query_builder.get_sql(); let query = sqlx::query(&update_sql); let query = query_builder.query(query); - let _ = query.execute(self.conn()).await?; - Ok(()) + let result = query.execute(self.conn()).await?; + Ok(result.rows_affected() > 0) + } + + /// Atomically advance a graph status according to its evidence source. + /// + /// 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. + pub async fn transition_graph_status( + &mut self, + instance_id: Uuid, + graph_id: Uuid, + target: GraphStatus, + source: GraphStatusSource, + sub_status: Option, + ) -> anyhow::Result { + if !target.is_protocol_status() { + anyhow::bail!("frontend-only graph status {target} cannot be persisted"); + } + + let allowed_from = target.allowed_transition_from(source); + if allowed_from.is_empty() { + // Some verified scans merely observe a baseline state (for + // example, an operator-pre-signed graph). There is no authorized + // predecessor edge to write in that case; return the current + // projection without mutating it. + return self.graph_status_transition_outcome(instance_id, graph_id, target).await; + } + + let allowed_from: Vec = allowed_from.iter().map(ToString::to_string).collect(); + let current_time = get_current_timestamp_secs(); + let mut query_builder = QueryBuilder::update("graph"); + query_builder.set_field("status", QueryParam::Text(target.to_string())); + if let Some(sub_status) = sub_status.as_ref() { + query_builder.set_field("sub_status", QueryParam::Text(sub_status.clone())); + } + query_builder.set_field("status_updated_at", QueryParam::Int(current_time)); + query_builder.set_field("updated_at", QueryParam::Int(current_time)); + query_builder.and_where( + "hex(graph_id) = ? COLLATE NOCASE", + Some(QueryParam::Text(hex::encode(graph_id))), + ); + query_builder.and_where( + "hex(instance_id) = ? COLLATE NOCASE", + Some(QueryParam::Text(hex::encode(instance_id))), + ); + query_builder.and_where_in("status", &allowed_from, false); + + let update_sql = query_builder.get_sql(); + let query = query_builder.query(sqlx::query(&update_sql)); + if query.execute(self.conn()).await?.rows_affected() > 0 { + return Ok(GraphStatusTransitionOutcome::Applied); + } + + // A zero-row conditional update has two distinct meanings: this can + // be an idempotent replay, or another writer may have moved the row to + // a state which rejects this transition. The follow-up read reports + // that distinction; it never authorizes a write. + let outcome = self.graph_status_transition_outcome(instance_id, graph_id, target).await?; + if !matches!(outcome, GraphStatusTransitionOutcome::AlreadyCurrent) { + return Ok(outcome); + } + + let Some(sub_status) = sub_status else { + return Ok(GraphStatusTransitionOutcome::AlreadyCurrent); + }; + + let current_time = get_current_timestamp_secs(); + let result = sqlx::query( + "UPDATE graph SET sub_status = ?, updated_at = ? \ + WHERE graph_id = ? AND instance_id = ? AND status = ?", + ) + .bind(sub_status) + .bind(current_time) + .bind(graph_id) + .bind(instance_id) + .bind(target.to_string()) + .execute(self.conn()) + .await?; + if result.rows_affected() > 0 { + return Ok(GraphStatusTransitionOutcome::AlreadyCurrent); + } + + // The row may have changed between the outcome read and the optional + // sub-status update. Re-read so a concurrent transition is never + // misreported as an idempotent replay or a missing graph. + self.graph_status_transition_outcome(instance_id, graph_id, target).await + } + + async fn graph_status_transition_outcome( + &mut self, + instance_id: Uuid, + graph_id: Uuid, + target: GraphStatus, + ) -> anyhow::Result { + let Some(current_graph) = self.find_graph(&graph_id).await? else { + return Ok(GraphStatusTransitionOutcome::NotFound); + }; + if current_graph.instance_id != instance_id { + return Ok(GraphStatusTransitionOutcome::NotFound); + } + let current = GraphStatus::from_str(¤t_graph.status).map_err(|_| { + anyhow::anyhow!( + "graph {graph_id} has invalid persisted status {}", + current_graph.status + ) + })?; + Ok(if current == target { + GraphStatusTransitionOutcome::AlreadyCurrent + } else { + GraphStatusTransitionOutcome::Rejected { current } + }) } pub async fn find_graph(&mut self, graph_id: &Uuid) -> anyhow::Result> { @@ -1342,6 +1646,7 @@ impl<'a> StorageProcessor<'a> { status, sub_status, operator_pubkey, + definition_hash, cur_prekickoff_txid, next_prekickoff, force_skip_kickoff_txid, @@ -1529,47 +1834,6 @@ impl<'a> StorageProcessor<'a> { Ok(()) } - pub async fn update_graphs_status_with_instance_id( - &mut self, - instance_id: Uuid, - ignore_graph_id: Option, - status: &str, - ) -> anyhow::Result<()> { - let current_time = get_current_timestamp_secs(); - if let Some(ignore_graph_id) = ignore_graph_id { - sqlx::query!( - "UPDATE graph - SET status = ?, - status_updated_at = ?, - updated_at = ? - WHERE instance_id = ? - AND graph_id != ?", - status, - current_time, - current_time, - instance_id, - ignore_graph_id - ) - .execute(self.conn()) - .await?; - } else { - sqlx::query!( - "UPDATE graph - SET status = ?, - status_updated_at = ?, - updated_at = ? - WHERE instance_id = ?", - status, - current_time, - current_time, - instance_id, - ) - .execute(self.conn()) - .await?; - } - Ok(()) - } - pub async fn find_graph_neighbor_ids( &mut self, graph_id: Uuid, @@ -1796,14 +2060,17 @@ impl<'a> StorageProcessor<'a> { state: String, ) -> anyhow::Result { let current_time = get_current_timestamp_secs(); - let res = sqlx::query!( - "Update message Set state = ?, updated_at = ? WHERE message_id = ? AND message_version = ?", - state, - current_time, - message_id, - message_version - - ).execute(self.conn()).await?; + let res = sqlx::query( + "UPDATE message \ + SET state = ?, updated_at = ? \ + WHERE message_id = ? AND message_version = ? AND state != 'Cancelled'", + ) + .bind(state) + .bind(current_time) + .bind(message_id) + .bind(message_version) + .execute(self.conn()) + .await?; Ok(res.rows_affected() > 0) } @@ -1818,23 +2085,31 @@ impl<'a> StorageProcessor<'a> { let current_time = get_current_timestamp_secs(); let res = match msg_type { Some(msg_type) => { - sqlx::query!( - "Update message Set state = ?, updated_at = ? WHERE business_id = ? AND msg_type = ? AND state = ?", - new_state, - current_time, - business_id, - msg_type, - old_state - ).execute(self.conn()).await? + sqlx::query( + "UPDATE message \ + SET state = ?, updated_at = ? \ + WHERE business_id = ? AND msg_type = ? AND state = ? AND state != 'Cancelled'", + ) + .bind(new_state) + .bind(current_time) + .bind(business_id) + .bind(msg_type) + .bind(old_state) + .execute(self.conn()) + .await? } None => { - sqlx::query!( - "Update message Set state = ?, updated_at = ? WHERE business_id = ? AND state = ?", - new_state, - current_time, - business_id, - old_state - ).execute(self.conn()).await? + sqlx::query( + "UPDATE message \ + SET state = ?, updated_at = ? \ + WHERE business_id = ? AND state = ? AND state != 'Cancelled'", + ) + .bind(new_state) + .bind(current_time) + .bind(business_id) + .bind(old_state) + .execute(self.conn()) + .await? } }; Ok(res.rows_affected() > 0) @@ -1884,10 +2159,9 @@ impl<'a> StorageProcessor<'a> { business_id: &Uuid, msg_type: &str, ) -> anyhow::Result> { - let res = sqlx::query_as!( - Message, + let row = sqlx::query( "SELECT message_id, - business_id AS \"business_id:Uuid\", + business_id, from_peer, actor, msg_type, @@ -1895,24 +2169,24 @@ impl<'a> StorageProcessor<'a> { message_version, state, weight, - lock_time_until + lock_time_until, + created_at FROM message WHERE business_id = ? AND msg_type = ?", - business_id, - msg_type, ) + .bind(business_id) + .bind(msg_type) .fetch_optional(self.conn()) .await?; - Ok(res) + Ok(row.map(|row| message_from_row(&row)).transpose()?) } pub async fn find_messages_by_id( &mut self, message_id: &str, ) -> anyhow::Result> { - let res = sqlx::query_as!( - Message, + let row = sqlx::query( "SELECT message_id, - business_id AS \"business_id:Uuid\", + business_id, from_peer, actor, msg_type, @@ -1920,14 +2194,15 @@ impl<'a> StorageProcessor<'a> { message_version, state, weight, - lock_time_until + lock_time_until, + created_at FROM message WHERE message_id = ?", - message_id, ) + .bind(message_id) .fetch_optional(self.conn()) .await?; - Ok(res) + Ok(row.map(|row| message_from_row(&row)).transpose()?) } pub async fn filter_messages( @@ -1939,10 +2214,9 @@ impl<'a> StorageProcessor<'a> { limit: i64, offset: i64, ) -> anyhow::Result> { - let res = sqlx::query_as!( - Message, + let rows = sqlx::query( "SELECT message_id, - business_id AS \"business_id:Uuid\", + business_id, from_peer, actor, msg_type, @@ -1950,7 +2224,8 @@ impl<'a> StorageProcessor<'a> { message_version, state, weight, - lock_time_until + lock_time_until, + created_at FROM message WHERE state = ? AND weight >= ? @@ -1958,21 +2233,51 @@ impl<'a> StorageProcessor<'a> { AND updated_at >= ? ORDER BY created_at ASC LIMIT ? OFFSET ?", - state, - weight, - lock_time_until, - expired, - limit, - offset ) + .bind(state) + .bind(weight) + .bind(lock_time_until) + .bind(expired) + .bind(limit) + .bind(offset) .fetch_all(self.conn()) .await?; - Ok(res) + rows.into_iter() + .map(|row| message_from_row(&row)) + .collect::, _>>() + .map_err(Into::into) + } + + pub async fn get_message_queue_stats( + &mut self, + actor: &str, + now: i64, + ) -> anyhow::Result { + let row = sqlx::query( + r#"SELECT + COALESCE(SUM(CASE WHEN state = 'Pending' AND lock_time_until <= ? THEN 1 ELSE 0 END), 0) AS pending_ready, + COALESCE(SUM(CASE WHEN state = 'Pending' AND lock_time_until > ? THEN 1 ELSE 0 END), 0) AS pending_locked, + COALESCE(SUM(CASE WHEN state = 'Failed' THEN 1 ELSE 0 END), 0) AS failed, + MIN(CASE WHEN state = 'Pending' THEN created_at END) AS oldest_pending_at + FROM message + WHERE actor = ?"#, + ) + .bind(now) + .bind(now) + .bind(actor) + .fetch_one(self.conn()) + .await?; + Ok(MessageQueueStats { + pending_ready: row.try_get("pending_ready")?, + pending_locked: row.try_get("pending_locked")?, + failed: row.try_get("failed")?, + oldest_pending_at: row.try_get("oldest_pending_at")?, + }) } pub async fn upsert_message(&mut self, msg: Message) -> anyhow::Result { let current_time = get_current_timestamp_secs(); - let res = sqlx::query!( + let res = sqlx::query( r#"INSERT INTO message (message_id, business_id, from_peer, actor, msg_type, content, state, message_version, lock_time_until, weight, updated_at, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(message_id) DO UPDATE SET business_id = excluded.business_id, @@ -1984,26 +2289,65 @@ impl<'a> StorageProcessor<'a> { message_version = message.message_version + 1, lock_time_until = excluded.lock_time_until, weight = excluded.weight, - updated_at = excluded.updated_at"#, - msg.message_id, - msg.business_id, - msg.from_peer, - msg.actor, - msg.msg_type, - msg.content, - msg.state, - msg.message_version, - msg.lock_time_until, - msg.weight, - current_time, - current_time - - ) - .execute(self.conn()) + updated_at = excluded.updated_at + WHERE message.state != 'Cancelled'"#, + ) + .bind(msg.message_id) + .bind(msg.business_id) + .bind(msg.from_peer) + .bind(msg.actor) + .bind(msg.msg_type) + .bind(msg.content) + .bind(msg.state) + .bind(msg.message_version) + .bind(msg.lock_time_until) + .bind(msg.weight) + .bind(current_time) + .bind(current_time) + .execute(self.conn()) .await?; Ok(res.rows_affected() > 0) } + /// Record that a chain-derived graph message has been durably enqueued. + /// + /// Queue rows are intentionally pruned after their retention period, but + /// replaying an old SyncGraph must not recreate already-completed protocol + /// actions. The caller inserts this marker and its queue row in one + /// transaction. + pub async fn insert_graph_compensation_marker( + &mut self, + graph_id: Uuid, + message_id: &str, + ) -> anyhow::Result { + let result = sqlx::query( + "INSERT INTO graph_compensation_marker (graph_id, message_id, created_at) \ + VALUES (?, ?, ?) ON CONFLICT(graph_id, message_id) DO NOTHING", + ) + .bind(graph_id) + .bind(message_id) + .bind(get_current_timestamp_secs()) + .execute(self.conn()) + .await?; + Ok(result.rows_affected() > 0) + } + + pub async fn has_graph_compensation_marker( + &mut self, + graph_id: Uuid, + message_id: &str, + ) -> anyhow::Result { + let exists: i64 = sqlx::query_scalar( + "SELECT EXISTS(SELECT 1 FROM graph_compensation_marker \ + WHERE graph_id = ? AND message_id = ?)", + ) + .bind(graph_id) + .bind(message_id) + .fetch_one(self.conn()) + .await?; + Ok(exists != 0) + } + pub async fn upsert_pegin_instance_process_data( &mut self, pegin_instance_process_data: &PeginInstanceProcessData, @@ -2128,18 +2472,48 @@ impl<'a> StorageProcessor<'a> { pub async fn upsert_graph_raw_data( &mut self, + instance_id: Uuid, graph_raw_data: GraphRawData, + definition_hash: &str, ) -> anyhow::Result { - let result = sqlx::query!( - r#" - INSERT OR REPLACE INTO graph_raw_data (graph_id, raw_data, created_at, updated_at) - VALUES (?, ?, ?, ?) - "#, - graph_raw_data.graph_id, - graph_raw_data.raw_data, - graph_raw_data.created_at, - graph_raw_data.updated_at + if definition_hash.is_empty() { + anyhow::bail!( + "graph {} raw definition is missing its canonical definition hash", + graph_raw_data.graph_id + ); + } + let Some(graph) = self.find_graph(&graph_raw_data.graph_id).await? else { + anyhow::bail!( + "cannot store raw definition for missing graph {}", + graph_raw_data.graph_id + ); + }; + if graph.instance_id != instance_id { + anyhow::bail!( + "raw definition instance does not match graph {}: stored={}, incoming={instance_id}", + graph_raw_data.graph_id, + graph.instance_id + ); + } + if graph.definition_hash != definition_hash { + anyhow::bail!( + "raw definition hash does not match graph {}: stored={}, incoming={definition_hash}", + graph_raw_data.graph_id, + graph.definition_hash + ); + } + + let result = sqlx::query( + "INSERT INTO graph_raw_data (graph_id, raw_data, created_at, updated_at) + VALUES (?, ?, ?, ?) + ON CONFLICT(graph_id) DO UPDATE SET + raw_data = excluded.raw_data, + updated_at = excluded.updated_at", ) + .bind(graph_raw_data.graph_id) + .bind(graph_raw_data.raw_data) + .bind(graph_raw_data.created_at) + .bind(graph_raw_data.updated_at) .execute(self.conn()) .await?; @@ -2166,37 +2540,6 @@ impl<'a> StorageProcessor<'a> { Ok(row) } - pub async fn update_graph_raw_data( - &mut self, - graph_id: &Uuid, - raw_data: &str, - ) -> anyhow::Result { - let timestamp = get_current_timestamp_millis(); - - let result = sqlx::query!( - r#" - UPDATE graph_raw_data - SET raw_data = ?, updated_at = ? - WHERE graph_id = ? - "#, - raw_data, - timestamp, - graph_id - ) - .execute(self.conn()) - .await?; - - Ok(result.rows_affected()) - } - - pub async fn delete_graph_raw_data(&mut self, graph_id: &str) -> anyhow::Result { - let result = sqlx::query!(r#"DELETE FROM graph_raw_data WHERE graph_id = ?"#, graph_id) - .execute(self.conn()) - .await?; - - Ok(result.rows_affected()) - } - pub async fn find_watch_contract( &mut self, addr: &str, @@ -3147,7 +3490,7 @@ pub async fn create_local_db(db_path: &str) -> LocalDB { } #[cfg(test)] -mod sequencer_set_tests { +mod tests { use super::*; async fn setup_db() -> LocalDB { diff --git a/crates/store/src/schema.rs b/crates/store/src/schema.rs index f87f671b..baf08ef7 100644 --- a/crates/store/src/schema.rs +++ b/crates/store/src/schema.rs @@ -199,8 +199,8 @@ pub enum InstanceBridgeInStatus { // committee won't answer if userRequest is invalid(e.g. insufficient fee) CommitteesAnswered, // enough committee responsed & window expired UserBroadcastPeginPrepare, // user pegin prepare - Presigned, // all committee signed PeginConfirm - PresignedFailed, // includes operator and Committee presigns + Presigned, // enough graphs completed committee pre-signing + PresignedFailed, // required graph pre-signing did not finish in time RelayerL1Broadcasted, // PeginConfirm broadcast by relayer RelayerL2Minted, // success RelayerL2MintedFailed, @@ -300,6 +300,26 @@ pub enum GraphStatus { Disproving, } +/// The evidence that authorizes a graph status transition. +/// +/// Graph definitions, finalized Goat events, and chain reconciliation have +/// different authority. Keeping the source explicit prevents a generic +/// database update from accidentally becoming a state-transition bypass. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum GraphStatusSource { + Definition, + GoatEvent, + ChainReconcile, +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub enum GraphStatusTransitionOutcome { + Applied, + AlreadyCurrent, + Rejected { current: GraphStatus }, + NotFound, +} + impl GraphStatus { pub fn get_closed_status() -> Vec { vec![ @@ -329,47 +349,97 @@ impl GraphStatus { pub fn is_obsoleted(&self) -> bool { self.eq(&GraphStatus::Obsoleted) } - pub fn get_previous_status(&self) -> Option { - match self { - GraphStatus::OperatorPresigned => None, - GraphStatus::CommitteePresigned => Some(GraphStatus::OperatorPresigned), - GraphStatus::OperatorDataPushed => Some(GraphStatus::CommitteePresigned), - GraphStatus::PreKickoff => Some(GraphStatus::OperatorDataPushed), - GraphStatus::Skipped => Some(GraphStatus::OperatorDataPushed), - GraphStatus::Obsoleted => Some(GraphStatus::OperatorDataPushed), - GraphStatus::OperatorKickOff => Some(GraphStatus::PreKickoff), - GraphStatus::OperatorTake1 => Some(GraphStatus::OperatorKickOff), - GraphStatus::Challenge => Some(GraphStatus::OperatorKickOff), - GraphStatus::Disprove => Some(GraphStatus::Challenge), - GraphStatus::OperatorTake2 => Some(GraphStatus::Challenge), - // frontend use only - GraphStatus::Created => None, - GraphStatus::Presigned => Some(GraphStatus::Created), - GraphStatus::L2Recorded => Some(GraphStatus::Presigned), - GraphStatus::OperatorKickOffing => Some(GraphStatus::L2Recorded), - GraphStatus::Challenging => Some(GraphStatus::OperatorKickOffing), - GraphStatus::Disproving => Some(GraphStatus::Challenging), - } - } - pub fn is_before(&self, other: &GraphStatus) -> bool { - let mut current = *other; - while let Some(prev) = current.get_previous_status() { - if &prev == self { - return true; - } - current = prev; - } - false - } - pub fn is_after(&self, other: &GraphStatus) -> bool { - let mut current = *self; - while let Some(prev) = current.get_previous_status() { - if &prev == other { - return true; - } - current = prev; + + /// Whether this is a backend protocol state rather than a frontend-only + /// display alias. + pub fn is_protocol_status(self) -> bool { + matches!( + self, + Self::OperatorPresigned + | Self::CommitteePresigned + | Self::OperatorDataPushed + | Self::PreKickoff + | Self::OperatorKickOff + | Self::Challenge + | Self::Disprove + | Self::Obsoleted + | Self::Skipped + | Self::OperatorTake1 + | Self::OperatorTake2 + ) + } + + /// States from which `self` may be reached with the supplied evidence. + /// + /// These sets intentionally contain transitive predecessors. A chain scan + /// can observe several confirmed transactions in one pass, so requiring a + /// locally persisted intermediate state would make recovery after restart + /// impossible. Closed states are deliberately absent from all sets. + pub fn allowed_transition_from(self, source: GraphStatusSource) -> &'static [GraphStatus] { + use GraphStatus::*; + + const EARLY: &[GraphStatus] = &[OperatorPresigned, CommitteePresigned]; + const EARLY_OR_OBSOLETED: &[GraphStatus] = + &[OperatorPresigned, CommitteePresigned, Obsoleted]; + const PRE_KICKOFF: &[GraphStatus] = + &[OperatorPresigned, CommitteePresigned, OperatorDataPushed, Obsoleted]; + const KICKOFF: &[GraphStatus] = + &[OperatorPresigned, CommitteePresigned, OperatorDataPushed, PreKickoff, Obsoleted]; + const CHALLENGE: &[GraphStatus] = &[ + OperatorPresigned, + CommitteePresigned, + OperatorDataPushed, + PreKickoff, + Obsoleted, + OperatorKickOff, + ]; + const TAKE1: &[GraphStatus] = &[ + OperatorPresigned, + CommitteePresigned, + OperatorDataPushed, + PreKickoff, + Obsoleted, + OperatorKickOff, + ]; + const TAKE2_OR_DISPROVE: &[GraphStatus] = &[ + OperatorPresigned, + CommitteePresigned, + OperatorDataPushed, + PreKickoff, + Obsoleted, + OperatorKickOff, + Challenge, + ]; + const SKIPPED: &[GraphStatus] = + &[OperatorPresigned, CommitteePresigned, OperatorDataPushed, PreKickoff, Obsoleted]; + + match source { + GraphStatusSource::Definition => match self { + CommitteePresigned => &[OperatorPresigned], + _ => &[], + }, + GraphStatusSource::GoatEvent => match self { + OperatorDataPushed => EARLY, + OperatorTake1 => TAKE1, + OperatorTake2 | Disprove => TAKE2_OR_DISPROVE, + _ => &[], + }, + GraphStatusSource::ChainReconcile => match self { + CommitteePresigned => &[OperatorPresigned], + // Obsoleted is a provisional branch when GraphData was not + // yet visible. A later full chain scan may correct it, but + // ordinary Goat event replay may not. + OperatorDataPushed => EARLY_OR_OBSOLETED, + PreKickoff | Obsoleted => PRE_KICKOFF, + OperatorKickOff => KICKOFF, + Challenge => CHALLENGE, + OperatorTake1 => TAKE1, + OperatorTake2 | Disprove => TAKE2_OR_DISPROVE, + Skipped => SKIPPED, + OperatorPresigned | Created | Presigned | L2Recorded | OperatorKickOffing + | Challenging | Disproving => &[], + }, } - false } } @@ -386,6 +456,9 @@ pub struct Graph { pub status: String, // GraphStatus pub sub_status: String, // GraphStatus pub operator_pubkey: String, + /// Canonical hash of the immutable graph parameters. This binds the + /// runtime projection to exactly one transaction-graph definition. + pub definition_hash: String, pub next_prekickoff: Option, pub cur_prekickoff_txid: Option, pub force_skip_kickoff_txid: Option, @@ -448,6 +521,7 @@ pub struct Message { pub message_version: i64, pub weight: i64, pub lock_time_until: i64, + pub created_at: i64, } #[derive(Clone, FromRow, Debug, Serialize, Deserialize, Default)] diff --git a/node/Cargo.toml b/node/Cargo.toml index f805d25a..38ad959a 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -15,6 +15,10 @@ path = "src/bin/send_pegin_request.rs" name = "challenge" path = "src/bin/send_challenge.rs" +[[bin]] +name = "verifier-challenge" +path = "src/bin/send_verifier_challenge.rs" + [[bin]] name = "sequencer-set-publish" path = "src/bin/sequencer-set-publish.rs" @@ -93,7 +97,7 @@ prometheus-client = { workspace = true } esplora-client = { workspace = true } tracing = { workspace = true } -tracing-subscriber = { workspace = true, features = ["env-filter"] } +tracing-subscriber = { workspace = true, features = ["env-filter", "json"] } thiserror = "2.0" bitcoin = { workspace = true, features = ["serde"] } diff --git a/node/README.md b/node/README.md index 8bf705ea..4c897dc3 100644 --- a/node/README.md +++ b/node/README.md @@ -412,20 +412,27 @@ stateDiagram-v2 PreKickoff --> OperatorKickOff: Kickoff tx broadcast PreKickoff --> Skipped: Guardian intervention or force skip + Obsoleted --> OperatorKickOff: Kickoff tx observed on-chain after all + Obsoleted --> Skipped: Guardian intervention or force skip + OperatorKickOff --> OperatorTake1: Timelock expired without challenge OperatorKickOff --> Challenge: WatchtowerChallengeInit confirmed + OperatorKickOff --> Disprove: Guardian disprove detected before any Challenge opened - Challenge --> OperatorTake2: All challenge phases completed normally - Challenge --> Disprove: WT-Ack/BlockHash-Commit/Assert Timeout or Fraud scripts detected + Challenge --> OperatorTake2: Take2 tx confirmed and no disprove detected + Challenge --> Disprove: Guardian disprove, watchtower-flow disprove, verifier disprove, or unrecognized connector-D spend OperatorTake1 --> [*]: Happy Path complete OperatorTake2 --> [*]: Challenge Path complete Skipped --> [*]: Graph skipped - Obsoleted --> [*]: Graph obsoleted Disprove --> [*]: Dispute resolved ``` -**Description**: Graph goes through 9 main states. The normal flow starts from `OperatorPresigned`, proceeds through signature collection, data publishing, and Kickoff, finally completing via Take1 (Happy Path) or Take2 (Challenge Path). Abnormal cases enter `Obsoleted`, `Skipped`, or `Disprove` terminal states. +**Description**: Graph goes through 9 main states. The normal flow starts from `OperatorPresigned`, proceeds through signature collection, data publishing, and Kickoff, finally completing via Take1 (Happy Path) or Take2 (Challenge Path). `Skipped` and `Disprove` are terminal. + +**`Obsoleted` is not terminal.** It is a provisional status: the local node marks a graph `Obsoleted` when it observes the PreKickoff tx on-chain before graph data was posted (or the pegin becomes non-withdrawable), but if it later observes the actual kickoff tx confirm, the graph resumes into `OperatorKickOff` exactly as if it had come from `PreKickoff` — see `scan_graph_chain_state`, `node/src/utils.rs:1418-1434`, which checks `matches!(current_status, GraphStatus::PreKickoff | GraphStatus::Obsoleted)` for both the kickoff and force-skip cases. + +`OperatorKickOff --> Disprove` is a direct edge, distinct from the `Challenge --> Disprove` edge below: a guardian can detect and prove operator misbehavior (`detect_guardian_disprove`, `node/src/utils.rs:1237-1262`) before a watchtower ever opens a `Challenge` at all (`node/src/utils.rs:1448-1461`). ### Fig-04-2-Graph-Status-Transitions @@ -438,53 +445,44 @@ stateDiagram-v2 | `OperatorDataPushed` | `Obsoleted` | Pegin not withdrawable and no withdrawal request | | `PreKickoff` | `OperatorKickOff` | Kickoff tx broadcast successfully | | `PreKickoff` | `Skipped` | Guardian intervention or force skip | -| `OperatorKickOff` | `OperatorTake1` | Timelock expired without challenge | -| `OperatorKickOff` | `Challenge` | WatchtowerChallengeInit tx confirmed | -| `Challenge` | `OperatorTake2` | All sub-phases completed normally | -| `Challenge` | `Disprove` | Timeout or challenge failure detected | +| `Obsoleted` | `OperatorKickOff` | Kickoff tx broadcast successfully (Obsoleted is provisional, not terminal) | +| `Obsoleted` | `Skipped` | Guardian intervention or force skip | +| `OperatorKickOff` | `OperatorTake1` | Connector-A spent by the take1 tx | +| `OperatorKickOff` | `Challenge` | Connector-A spent by anything other than the take1 tx | +| `OperatorKickOff` | `Disprove` | Guardian disprove detected (bypasses `Challenge`) | +| `Challenge` | `OperatorTake2` | Take2 tx observed on-chain and no disprove detector fired | +| `Challenge` | `Disprove` | Any of: guardian disprove, watchtower-flow disprove (operator-challenge-nack or operator-commit-timeout tx), a verifier's assert answered by its matching disprove tx, or an unrecognized connector-D spend | -### Fig-04-3-Challenge-SubStatus-ER +Source: `scan_graph_chain_state`, `node/src/utils.rs:1328-1681` (the sole writer of these edges; verified by full read, 2026-07-17). -```mermaid -erDiagram - ChallengeSubStatus { - WatchtowerChallengeStatus watchtower_challenge_status - CommitBlockHashStatus commit_blockhash_status - AssertCommitStatus assert_commit_status - DisproveTxType disprove_type - int disprove_index - } +### Fig-04-3-Challenge-SubStatus - WatchtowerChallengeStatus { - enum None - enum OperatorInit - enum WatchtowerChallenge - enum WatchtowerChallengeTimeout - enum OperatorACKTimeout - enum WatchtowerChallengeNormalFinished - enum WatchtowerChallengeDisproveFinished - } +`ChallengeSubStatus` is **not** three parallel single-value status enums as earlier revisions of this document claimed — that shape does not exist in the code. The actual struct (`node/src/scheduled_tasks/graph_maintenance_tasks.rs:52-58`) is: - CommitBlockHashStatus { - enum None - enum WatchtowerChallengeProcessed - enum OperatorCommit - enum OperatorCommitTimeout - } +```rust +pub struct ChallengeSubStatus { + pub watchtower_challenge_status: Vec, // per-watchtower: true once that watchtower's challenge connector is spent + pub verifier_challenge_status: Vec, // per-verifier progress + pub disprove_type: Option, + pub disprove_index: i32, +} - AssertCommitStatus { - enum None - enum OperatorInit - enum OperatorCommit - enum OperatorCommitTimeout - } +pub enum VerifierChallengeStatus { None, VerifierAsserted, ProverAnswered, Disproved } - ChallengeSubStatus ||--|| WatchtowerChallengeStatus : contains - ChallengeSubStatus ||--|| CommitBlockHashStatus : contains - ChallengeSubStatus ||--|| AssertCommitStatus : contains +pub enum DisproveTxType { Disprove, QuickChallenge, ChallengeIncompleteKickoff, PubinDisprove, OperatorChallengeNack, OperatorCommitTimeout } ``` -**Description**: `ChallengeSubStatus` aggregates three parallel status trackers. Only when `watchtower_challenge_status == NormalFinished`, `commit_blockhash_status == OperatorCommit`, and `assert_commit_status == OperatorCommit` can the system transition to `OperatorTake2` state. +`watchtower_challenge_status` and `verifier_challenge_status` are **observational bookkeeping only** — they are recorded for the frontend/API (`node/src/rpc_service/bitvm.rs:672-693`, collapsed there into a simpler `SimpleChallengeSubStatus{None, WatchtowerChallenge, Assert}` view) but are never read by anything that decides `GraphStatus`. In particular, `ChallengeSubStatus::is_watchtower_challenge_success` (`graph_maintenance_tasks.rs:98-105`) has no call sites anywhere in the repo. The real `Challenge --> OperatorTake2` gate is purely `tx_on_chain(take2_txid)` after none of the four disprove detectors fired (`node/src/utils.rs:1661-1670`); the operator's actual authorization to broadcast take2 is enforced by Bitcoin-script timelocks (`detect_take2`, `graph_maintenance_tasks.rs:1089-1233`), not by any application-level quorum over watchtower or verifier counts. + +### Known gap: no ordering guarantee between chain-scan and L2-event writers + +`GraphStatus` is written from two independently-scheduled places with no inherent coordination between them: +- `scan_graph_chain_state` above (Bitcoin-poll derived), and +- the GoatChain L2-event watcher, `node/src/scheduled_tasks/event_watch_task.rs:392-502`, which writes `OperatorDataPushed`/`OperatorTake1`/`OperatorTake2`/`Disprove` directly via `StorageProcessor::update_graph` with no precondition on the graph's current status. + +Both currently go through a raw `UPDATE graph SET status = ?` (`crates/store/src/localdb.rs`) with no guard preventing a closed/terminal status (`GraphStatus::is_closed()`: `OperatorTake1`, `OperatorTake2`, `Skipped`, `Disprove`) from being overwritten by a later, differently-ordered write from the other subsystem — e.g. a `Disprove` status can be silently reverted by a stale `PostGraphDataEvent` replay. **This is an open bug, not yet fixed in code.** + +`node/tla/GraphLifecycle.tla` / `GraphLifecycle.cfg` has the machine-checked counterexample (`OperatorTake1 -> OperatorTake2` in 2 steps). A verified fix design exists and is formally proven correct — `GraphLifecycleFixed.cfg` (atomic guard: fold the check into the `UPDATE`'s `WHERE status IN (...)` clause rather than a separate read-then-decide-then-write) passes all safety and liveness properties, and `GraphLifecycleFineGrainedFixed.tla` additionally proves that atomicity is necessary (a naive read-then-write version of the same guard, modeled in `GraphLifecycleFineGrained.tla`, still fails). Applying that design to `node/src/utils.rs`/`crates/store/src/localdb.rs` is tracked as follow-up work, not part of this audit pass. --- diff --git a/node/src/action.rs b/node/src/action.rs index 4aae08fc..bf0c13ee 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -8,7 +8,7 @@ use crate::middleware::AllBehaviours; use crate::rpc_service::current_time_secs; use crate::utils::*; use alloy::primitives::Address as EvmAddress; -use anyhow::{Context, Result, anyhow}; +use anyhow::{Context, Result, anyhow, bail}; use bitcoin::{PublicKey, Txid}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{BabeBundleBuilder, CACSetupPackage}; @@ -23,9 +23,9 @@ use musig2::{PartialSignature, PubNonce}; use secp256k1::schnorr::Signature as SchnorrSignature; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use std::time::Instant; use store::MessageState; use store::localdb::LocalDB; -use tracing::warn; use uuid::Uuid; #[derive(Serialize, Deserialize, Clone)] @@ -80,6 +80,55 @@ pub enum GOATMessageContent { Tick, } +impl GOATMessageContent { + /// Stable message name for logs/metrics. Keep this independent of `Debug`, whose + /// output can include protocol payloads (and, for proofs, be very large). + pub fn event_type(&self) -> &'static str { + match self { + Self::PeginRequest(_) => "PeginRequest", + Self::CreateGraph(_) => "CreateGraph", + Self::ConfirmInstance(_) => "ConfirmInstance", + Self::InitGraph(_) => "InitGraph", + Self::GenCircuits(_) => "GenCircuits", + Self::CutCircuits(_) => "CutCircuits", + Self::SolderingProofReady(_) => "SolderingProofReady", + Self::VerifierGraphParamsEndorsement(_) => "VerifierGraphParamsEndorsement", + Self::NonceGeneration(_) => "NonceGeneration", + Self::CommitteePresign(_) => "CommitteePresign", + Self::EndorseGraph(_) => "EndorseGraph", + Self::GraphFinalize(_) => "GraphFinalize", + Self::PeginConfirmNonce(_) => "PeginConfirmNonce", + Self::PeginConfirmPartialSig(_) => "PeginConfirmPartialSig", + Self::PostReady(_) => "PostReady", + Self::KickoffReady(_) => "KickoffReady", + Self::KickoffSent(_) => "KickoffSent", + Self::PreKickoffSent(_) => "PreKickoffSent", + Self::ChallengeSent(_) => "ChallengeSent", + Self::WatchtowerChallengeInitSent(_) => "WatchtowerChallengeInitSent", + Self::WatchtowerChallengeSent(_) => "WatchtowerChallengeSent", + Self::WatchtowerChallengeTimeout(_) => "WatchtowerChallengeTimeout", + Self::NackReady(_) => "NackReady", + Self::OperatorCommitPubinReady(_) => "OperatorCommitPubinReady", + Self::OperatorCommitPubinTimeout(_) => "OperatorCommitPubinTimeout", + Self::AssertReady(_) => "AssertReady", + Self::AssertSent(_) => "AssertSent", + Self::ChallengeAssertSent(_) => "ChallengeAssertSent", + Self::WronglyChallengeTimeout(_) => "WronglyChallengeTimeout", + Self::DisproveSent(_) => "DisproveSent", + Self::Take1Ready(_) => "Take1Ready", + Self::Take1Sent(_) => "Take1Sent", + Self::Take2Ready(_) => "Take2Ready", + Self::Take2Sent(_) => "Take2Sent", + Self::RequestNodeInfo(_) => "RequestNodeInfo", + Self::ResponseNodeInfo(_) => "ResponseNodeInfo", + Self::SyncGraphRequest(_) => "SyncGraphRequest", + Self::SyncGraph(_) => "SyncGraph", + Self::InstanceDiscarded(_) => "InstanceDiscarded", + Self::Tick => "Tick", + } + } +} + /// Pegin #[derive(Serialize, Deserialize, Clone)] @@ -389,20 +438,37 @@ pub async fn handle_self_p2p_msg( message: &[u8], ) -> Result<()> { if id != GOATMessage::default_message_id() { - tracing::warn!("handle_self_p2p_msg received unexpected message id: {:?}", id); + tracing::warn!( + event = "local_message_queue", + outcome = "unexpected_message_id", + message_id = ?id, + "ignoring local queue trigger with an unexpected message id" + ); return Ok(()); } let message = GOATMessage::deserialize_message(message).await?; tracing::info!( - "Got self p2p message: {} with id: {} from peer: {:?}", - &message.actor.to_string(), - id, - from_peer_id + event = "local_message_queue", + outcome = "trigger_received", + role = %message.actor, + message_type = message.content.event_type(), + message_id = ?id, + from_peer_id = %from_peer_id, + "received local queue trigger" ); let messages = pop_batch_local_unhandle_msg(local_db, actor.clone(), current_time_secs(), 0, 50).await?; + tracing::info!( + event = "local_message_queue", + outcome = "batch_loaded", + role = %actor, + batch_size = messages.len(), + "loaded pending local messages" + ); for message in messages { + let queue_wait_secs = current_time_secs().saturating_sub(message.created_at); + let started_at = Instant::now(); match recv_and_dispatch( swarm, local_db, @@ -419,19 +485,53 @@ pub async fn handle_self_p2p_msg( { Ok(_) => { let mut storage_processor = local_db.acquire().await?; - storage_processor + let state_updated = storage_processor .update_messages_state( &message.message_id, message.message_version, MessageState::Processed.to_string(), ) .await?; + if state_updated { + tracing::info!( + event = "local_message_queue", + outcome = "processed", + role = %actor, + business_id = %message.business_id, + queued_message_id = %message.message_id, + message_type = %message.msg_type, + queue_wait_secs, + elapsed_ms = started_at.elapsed().as_millis() as u64, + "processed local message" + ); + } else { + tracing::warn!( + event = "local_message_queue", + outcome = "state_update_conflict", + role = %actor, + business_id = %message.business_id, + queued_message_id = %message.message_id, + message_type = %message.msg_type, + queue_wait_secs, + elapsed_ms = started_at.elapsed().as_millis() as u64, + "local message handler completed but its processed state was not persisted" + ); + } } Err(err) => { let lock_time = 600; - warn!( - "fail to process message:{}: {} will lock seconds {lock_time}", - message.message_id, err + tracing::warn!( + event = "local_message_queue", + outcome = "deferred", + role = %actor, + business_id = %message.business_id, + queued_message_id = %message.message_id, + message_type = %message.msg_type, + retry_after_secs = lock_time, + queue_wait_secs, + elapsed_ms = started_at.elapsed().as_millis() as u64, + error = %err, + "failed to process local message; deferred for retry" ); let mut storage_processor = local_db.acquire().await?; storage_processor @@ -470,6 +570,10 @@ pub async fn recv_and_dispatch( // Determine whether the message comes from this node itself to optionally skip validations let is_self_peer = get_local_node_info().peer_id == from_peer_id.to_string(); let message = GOATMessage::deserialize_message(message).await?; + let message_type = message.content.event_type(); + let role = actor.to_string(); + let from_peer_id_string = from_peer_id.to_string(); + let started_at = Instant::now(); let mut handler_ctx = HandlerContext { swarm, local_db, @@ -482,10 +586,34 @@ pub async fn recv_and_dispatch( id, is_self_peer, }; - handle_dispatch(&mut handler_ctx, message.content()).await + let result = handle_dispatch(&mut handler_ctx, message.content()).await; + match &result { + Ok(()) => tracing::info!( + event = "message_dispatch_result", + outcome = "handled", + role, + message_type, + from_peer_id = %from_peer_id_string, + is_self_peer, + elapsed_ms = started_at.elapsed().as_millis() as u64, + "message dispatch completed" + ), + Err(err) => tracing::warn!( + event = "message_dispatch_result", + outcome = "failed", + role, + message_type, + from_peer_id = %from_peer_id_string, + is_self_peer, + elapsed_ms = started_at.elapsed().as_millis() as u64, + error = %err, + "message dispatch failed" + ), + } + result } -pub async fn try_finalize_graph( +pub(crate) async fn try_finalize_graph( swarm: &mut Swarm, local_db: &LocalDB, goat_client: &GOATClient, @@ -493,7 +621,7 @@ pub async fn try_finalize_graph( graph_id: Uuid, graph: Option<&SimplifiedBitvmGcGraph>, broadcast_graph_finalize: bool, -) -> Result<()> { +) -> Result> { let endorsements = get_committee_endorsements_for_graph(local_db, instance_id, graph_id).await?; let params_endorsements = @@ -516,6 +644,15 @@ pub async fn try_finalize_graph( BitvmGcGraph::from_simplified(&g)? } }; + if graph.parameters.instance_parameters.instance_id != instance_id + || graph.parameters.graph_id != graph_id + { + bail!( + "refuse to finalize graph {instance_id}:{graph_id} with mismatched graph parameters {}:{}", + graph.parameters.instance_parameters.instance_id, + graph.parameters.graph_id + ); + } let pub_nonces = order_committee_values(&committee_pubkeys, pub_nonoces, "graph committee pub nonces")?; let agg_nonces = nonces_aggregation(&pub_nonces)?; @@ -527,7 +664,9 @@ pub async fn try_finalize_graph( let committee_sig_for_graph = signature_aggregation(&partial_sigs, &agg_nonces, &graph)?; push_committee_pre_signatures(&mut graph, &committee_sig_for_graph)?; let simplified_graph = graph.to_simplified()?; - store_graph(local_db, &simplified_graph).await?; + let store_outcome = store_finalized_graph_if_needed(local_db, &simplified_graph).await?; + mark_graph_as_endorsed(local_db, instance_id, graph_id).await?; + try_transition_instance_to_presigned(local_db, instance_id).await?; if broadcast_graph_finalize { let message_content = GOATMessageContent::GraphFinalize(GraphFinalize { instance_id, @@ -539,21 +678,44 @@ pub async fn try_finalize_graph( }); send_to_peer(swarm, GOATMessage::new(Actor::All, message_content)).await?; } + return Ok(Some((graph, store_outcome))); } - Ok(()) + Ok(None) } pub async fn send_to_peer( swarm: &mut Swarm, message: GOATMessage, ) -> Result { - let actor = message.actor.to_string(); - let topic = crate::middleware::get_topic_name(&actor); + let target_actor = message.actor.to_string(); + let message_type = message.content.event_type(); + let topic = crate::middleware::get_topic_name(&target_actor); let gossipsub_topic = gossipsub::IdentTopic::new(topic); - Ok(swarm - .behaviour_mut() - .gossipsub - .publish(gossipsub_topic, message.serialize_message().await?)?) + let serialized = message.serialize_message().await?; + match swarm.behaviour_mut().gossipsub.publish(gossipsub_topic, serialized) { + Ok(message_id) => { + tracing::info!( + event = "p2p_message_publish", + outcome = "published", + target_actor, + message_type, + message_id = ?message_id, + "published protocol message" + ); + Ok(message_id) + } + Err(err) => { + tracing::warn!( + event = "p2p_message_publish", + outcome = "failed", + target_actor, + message_type, + error = %err, + "failed to publish protocol message" + ); + Err(err.into()) + } + } } pub async fn push_local_unhandled_messages( @@ -592,21 +754,49 @@ pub(crate) async fn get_graph_or_defer( Some(g) => Ok(Some(g)), None => { // Ask for sync and push to local queue with a short retry delay - if let Err(e) = + let sync_request_outcome = if let Err(error) = try_send_sync_graph_request(swarm, goat_client, instance_id, graph_id).await { - tracing::warn!("Failed to send SyncGraphRequest for {instance_id}:{graph_id}: {e}"); - } + tracing::warn!( + event = "graph_resolution", + outcome = "sync_request_failed", + instance_id = %instance_id, + graph_id = %graph_id, + message_type = message.content.event_type(), + error_class = "p2p", + error = %error, + "failed to request graph synchronization" + ); + "failed" + } else { + "submitted" + }; let delay_secs: usize = 60; // 1 min default retry - if let Err(e) = + if let Err(error) = push_local_unhandled_messages(local_db, graph_id, message, delay_secs).await { - tracing::warn!( - "Failed to enqueue deferred message for {instance_id}:{graph_id}: {e}" + tracing::error!( + event = "graph_resolution", + outcome = "defer_failed", + instance_id = %instance_id, + graph_id = %graph_id, + message_type = message.content.event_type(), + retry_after_secs = delay_secs, + error_class = "database", + error = %error, + "failed to enqueue message while graph is missing" ); + return Err(error).context("failed to defer message while graph is missing"); } tracing::info!( - "Graph missing locally for {instance_id}:{graph_id}; requested sync and deferred" + event = "graph_resolution", + outcome = "deferred_missing_graph", + instance_id = %instance_id, + graph_id = %graph_id, + message_type = message.content.event_type(), + sync_request_outcome, + retry_after_secs = delay_secs, + "graph missing locally; requested sync and deferred message" ); Ok(None) } diff --git a/node/src/bin/mock_rpc.rs b/node/src/bin/mock_rpc.rs index c9e08644..e004d361 100644 --- a/node/src/bin/mock_rpc.rs +++ b/node/src/bin/mock_rpc.rs @@ -21,9 +21,11 @@ use clap::Parser; use client::Utxo; use prometheus_client::registry::Registry; use secp256k1::Secp256k1; +use store::localdb::{GraphRuntimeUpdate, StorageProcessor}; use store::{ BridgeOutGlobalStats, GoatTxProcessingStatus, GoatTxRecord, GoatTxType, Graph, GraphStatus, - Instance, InstanceBridgeInStatus, InstanceBridgeOutStatus, Node, UInt64Array3, create_local_db, + GraphStatusSource, Instance, InstanceBridgeInStatus, InstanceBridgeOutStatus, Node, + UInt64Array3, create_local_db, }; use tokio::signal; use tokio_util::sync::CancellationToken; @@ -211,6 +213,7 @@ fn seeded_graph( status: status.to_string(), sub_status, operator_pubkey: operator_pubkey.to_string(), + definition_hash: format!("mock-{graph_id}"), init_withdraw_tx_hash, bridge_out_start_at: now - 300, status_updated_at: now - 120, @@ -221,6 +224,52 @@ fn seeded_graph( } } +async fn seed_graph_runtime(tx: &mut StorageProcessor<'_>, graph: &Graph) -> Result<()> { + let target_status = GraphStatus::from_str(&graph.status) + .map_err(|_| anyhow::anyhow!("invalid mock graph status {}", graph.status))?; + let sub_status = graph.sub_status.clone(); + let challenge_txid = graph.challenge_txid.clone(); + let init_withdraw_tx_hash = graph.init_withdraw_tx_hash.clone(); + let bridge_out_start_at = graph.bridge_out_start_at; + let proceed_withdraw_height = graph.proceed_withdraw_height; + + let mut definition = graph.clone(); + definition.status = GraphStatus::OperatorPresigned.to_string(); + definition.sub_status.clear(); + definition.challenge_txid = None; + definition.init_withdraw_tx_hash = None; + definition.bridge_out_start_at = 0; + definition.proceed_withdraw_height = 0; + tx.upsert_graph_definition(&definition).await?; + + if target_status != GraphStatus::OperatorPresigned { + tx.transition_graph_status( + graph.instance_id, + graph.graph_id, + target_status, + GraphStatusSource::ChainReconcile, + (!sub_status.is_empty()).then_some(sub_status), + ) + .await?; + } + + let mut runtime = GraphRuntimeUpdate::new(graph.instance_id, graph.graph_id); + if let Some(challenge_txid) = challenge_txid { + runtime = runtime.with_challenge_txid(challenge_txid); + } + if let Some(init_withdraw_tx_hash) = init_withdraw_tx_hash { + runtime = runtime.with_init_withdraw_tx_hash(init_withdraw_tx_hash); + } + if bridge_out_start_at != 0 { + runtime = runtime.with_bridge_out_start_at(bridge_out_start_at); + } + if proceed_withdraw_height != 0 { + runtime = runtime.with_proceed_withdraw_height(proceed_withdraw_height); + } + tx.update_graph_runtime(&runtime).await?; + Ok(()) +} + async fn seed_mock_data( local_db: &store::localdb::LocalDB, current_peer_id: &str, @@ -294,7 +343,7 @@ async fn seed_mock_data( tx.upsert_instance(&instance).await?; } for graph in [ready_graph, challenge_graph] { - tx.upsert_graph(&graph).await?; + seed_graph_runtime(&mut tx, &graph).await?; } tx.upsert_goat_tx_record(&GoatTxRecord { instance_id: bridge_out_instance_id, diff --git a/node/src/bin/send_verifier_challenge.rs b/node/src/bin/send_verifier_challenge.rs new file mode 100644 index 00000000..5ba8dbb5 --- /dev/null +++ b/node/src/bin/send_verifier_challenge.rs @@ -0,0 +1,85 @@ +//! verifier-challenge: force a verifier ChallengeAssert transaction via node API. +//! +//! Purpose: +//! - Call the verifier node's test endpoint to broadcast its ChallengeAssert +//! transaction after an operator assert. +//! +//! Env: +//! - BITVM_SECRET: the node's secret key, used to sign the auth headers +//! +//! Args: +//! - --rpc-url: verifier node API base URL (default: http://localhost:8080) +//! - --graph-id: target graph UUID +//! +//! Example: +//! - cargo run -p bitvm-noded --bin verifier-challenge -- \ +//! --rpc-url http://localhost:8910 \ +//! --graph-id + +use anyhow::{Context, Result}; +use bitvm_noded::env::get_bitvm_key; +use bitvm_noded::rpc_service::auth::{ + AUTH_SIGNATURE_HEADER, AUTH_TIMESTAMP_HEADER, sign_request_auth, +}; +use clap::Parser; +use serde::Deserialize; + +#[derive(Debug, Parser)] +#[command( + name = "verifier-challenge", + version, + about = "Force a verifier ChallengeAssert transaction for a graph (via node API)", + long_about = "Force a verifier ChallengeAssert transaction for a graph via the verifier node's REST API.\n\nThe verifier node must be running and reachable at the given --rpc-url." +)] +struct Args { + /// Graph UUID to challenge + #[arg(long)] + graph_id: uuid::Uuid, + + /// Verifier node API base URL + #[arg(long, default_value = "http://localhost:8080")] + rpc_url: String, +} + +#[derive(Debug, Deserialize)] +struct SendVerifierChallengeResponse { + challenge_assert_txid: String, + verifier_index: usize, +} + +#[tokio::main] +async fn main() -> Result<()> { + dotenv::dotenv().ok(); + let args = Args::parse(); + let url = format!( + "{}/v1/graphs/{}/send-verifier-challenge", + args.rpc_url.trim_end_matches('/'), + args.graph_id + ); + + let keypair = get_bitvm_key().context("failed to load BITVM_SECRET")?; + let (timestamp, signature) = sign_request_auth(&keypair); + + let client = reqwest::Client::new(); + let resp = client + .post(&url) + .header(AUTH_TIMESTAMP_HEADER, ×tamp) + .header(AUTH_SIGNATURE_HEADER, &signature) + .send() + .await + .with_context(|| format!("failed to reach verifier node API at {url}"))?; + + if !resp.status().is_success() { + let status = resp.status(); + let body = resp.text().await.unwrap_or_default(); + anyhow::bail!("API returned {status}: {body}"); + } + + let body: SendVerifierChallengeResponse = + resp.json().await.context("failed to parse API response")?; + println!( + "Verifier ChallengeAssert tx broadcasted: {} (verifier_index={})", + body.challenge_assert_txid, body.verifier_index + ); + Ok(()) +} diff --git a/node/src/handle.rs b/node/src/handle.rs index 480b43a9..3ed9c50b 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -235,13 +235,33 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent .await } ( - GOATMessageContent::CreateGraph(CreateGraph { instance_id, graph_id, graph, .. }), + GOATMessageContent::CreateGraph(CreateGraph { + instance_id, + graph_id, + graph_nonce, + graph, + }), Actor::Verifier, - ) => handle_create_graph_verifier(ctx, *instance_id, *graph_id, graph).await, + ) => handle_create_graph_verifier(ctx, *instance_id, *graph_id, *graph_nonce, graph).await, ( - GOATMessageContent::CreateGraph(CreateGraph { instance_id, graph_id, graph, .. }), + GOATMessageContent::CreateGraph(CreateGraph { + instance_id, + graph_id, + graph_nonce, + graph, + }), Actor::Committee, - ) => handle_create_graph_committee(ctx, *instance_id, *graph_id, graph, content).await, + ) => { + handle_create_graph_committee( + ctx, + *instance_id, + *graph_id, + *graph_nonce, + graph, + content, + ) + .await + } ( GOATMessageContent::VerifierGraphParamsEndorsement(VerifierGraphParamsEndorsement { instance_id, @@ -374,10 +394,10 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent GOATMessageContent::GraphFinalize(GraphFinalize { instance_id, graph_id, + graph_nonce, graph, endorse_sigs, params_endorse_sigs, - .. }), Actor::Committee, ) => { @@ -385,6 +405,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent ctx, *instance_id, *graph_id, + *graph_nonce, graph, endorse_sigs, params_endorse_sigs, @@ -395,10 +416,10 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent GOATMessageContent::GraphFinalize(GraphFinalize { instance_id, graph_id, + graph_nonce, graph, endorse_sigs, params_endorse_sigs, - .. }), _, ) => { @@ -406,6 +427,7 @@ pub async fn dispatch(ctx: &mut HandlerContext<'_>, content: &GOATMessageContent ctx, *instance_id, *graph_id, + *graph_nonce, graph, endorse_sigs, params_endorse_sigs, @@ -659,6 +681,32 @@ fn make_message(ctx: &HandlerContext<'_>, content: &GOATMessageContent) -> GOATM GOATMessage::new(ctx.actor.clone(), content.clone()) } +/// A graph-bearing message has two identity representations: its envelope and +/// the signed graph parameters. Never use one to read/write local state while +/// using the other to reconstruct or scan the graph. +fn message_identity_matches( + message_kind: &str, + message_instance_id: Uuid, + message_graph_id: Uuid, + message_graph_nonce: Option, + graph_instance_id: Uuid, + graph_graph_id: Uuid, + graph_nonce: u64, +) -> bool { + let nonce_matches = message_graph_nonce.is_none_or(|nonce| nonce == graph_nonce); + if message_instance_id == graph_instance_id + && message_graph_id == graph_graph_id + && nonce_matches + { + return true; + } + + tracing::warn!( + "Ignore {message_kind}: message identity {message_instance_id}:{message_graph_id}:{message_graph_nonce:?} does not match graph parameters {graph_instance_id}:{graph_graph_id}:{graph_nonce}" + ); + false +} + /// Freezes the first accepted Verifiers and assigns deterministic graph slots. fn freeze_operator_candidates(state: &mut OperatorBabeSetupState) -> Result<()> { if state.frozen_verifier_pubkeys.is_some() { @@ -1001,58 +1049,62 @@ async fn refresh_and_compensate( ctx: &HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, - graph: Option<&BitvmGcGraph>, - scan_from_status: Option, - compensate_from_status: GraphStatus, -) -> Result<(GraphStatus, Option)> { - let (graph_status, sub_status, scan) = refresh_graph( - ctx.local_db, - ctx.btc_client, - ctx.goat_client, - instance_id, - graph_id, - graph, - scan_from_status, - None, - ) - .await?; + graph: &BitvmGcGraph, + compensation_anchor_status: GraphStatus, +) -> Result<(GraphStatus, Option, bool)> { + let refresh = + refresh_graph(ctx.local_db, ctx.btc_client, ctx.goat_client, instance_id, graph_id, graph) + .await?; + let graph_status = refresh.status; + let sub_status = refresh.sub_status; tracing::info!("Graph {graph_id} latest status: {graph_status}"); - compensate_graph_events( - ctx.local_db, - ctx.btc_client, - instance_id, - graph_id, - graph, - scan.as_ref(), - scan_from_status, - compensate_from_status, - graph_status, - ) - .await?; - Ok((graph_status, sub_status)) + if refresh.status_transition_accepted { + compensate_graph_events( + ctx.local_db, + ctx.btc_client, + instance_id, + graph_id, + graph, + refresh.scan.as_ref(), + compensation_anchor_status, + graph_status, + ) + .await?; + } + Ok((graph_status, sub_status, refresh.status_transition_accepted)) +} + +async fn refresh_newly_finalized_graph( + ctx: &HandlerContext<'_>, + instance_id: Uuid, + graph_id: Uuid, + graph: &BitvmGcGraph, +) -> Result<()> { + // Reconcile every verified GraphFinalize, including a duplicate. A prior + // attempt may have stored the finalized definition but failed before its + // first chain scan completed. + refresh_and_compensate(ctx, instance_id, graph_id, graph, GraphStatus::CommitteePresigned) + .await?; + Ok(()) } -async fn get_graph_and_status( +async fn get_graph_for_refresh( ctx: &HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, -) -> Result<(BitvmGcGraph, GraphStatus)> { +) -> Result { let graph = get_graph(ctx.local_db, instance_id, graph_id) .await? .ok_or_else(|| anyhow!("Graph not found for {instance_id}:{graph_id}"))?; - let graph = BitvmGcGraph::from_simplified(&graph)?; - let graph_start_status = get_graph_status(ctx.local_db, instance_id, graph_id) - .await? - .ok_or_else(|| anyhow!("Graph status not found for {instance_id}:{graph_id}"))?; - Ok((graph, graph_start_status)) + BitvmGcGraph::from_simplified(&graph) } -async fn get_graph_and_status_or_defer( +async fn get_graph_for_refresh_or_defer( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, message: &GOATMessage, -) -> Result> { +) -> Result> { let graph = match get_graph_or_defer( ctx.swarm, ctx.local_db, @@ -1066,11 +1118,7 @@ async fn get_graph_and_status_or_defer( Some(g) => g, None => return Ok(None), }; - let graph = BitvmGcGraph::from_simplified(&graph)?; - let graph_start_status = get_graph_status(ctx.local_db, instance_id, graph_id) - .await? - .ok_or_else(|| anyhow!("Graph status not found for {instance_id}:{graph_id}"))?; - Ok(Some((graph, graph_start_status))) + Ok(Some(BitvmGcGraph::from_simplified(&graph)?)) } async fn refresh_graph_status( @@ -1080,24 +1128,23 @@ async fn refresh_graph_status( message: Option<&GOATMessage>, compensate_from_status: GraphStatus, ) -> Result)>> { - let (graph, graph_start_status) = match message { + let graph = match message { Some(message) => { - match get_graph_and_status_or_defer(ctx, instance_id, graph_id, message).await? { + match get_graph_for_refresh_or_defer(ctx, instance_id, graph_id, message).await? { Some(v) => v, None => return Ok(None), } } - None => get_graph_and_status(ctx, instance_id, graph_id).await?, + None => get_graph_for_refresh(ctx, instance_id, graph_id).await?, }; - let (graph_status, sub_status) = refresh_and_compensate( - ctx, - instance_id, - graph_id, - Some(&graph), - Some(graph_start_status), - compensate_from_status, - ) - .await?; + let (graph_status, sub_status, status_transition_accepted) = + refresh_and_compensate(ctx, instance_id, graph_id, &graph, compensate_from_status).await?; + if !status_transition_accepted { + tracing::warn!( + "Ignore graph action for {instance_id}:{graph_id}: chain scan status was rejected or graph is missing" + ); + return Ok(None); + } Ok(Some((graph, graph_status, sub_status))) } @@ -1182,6 +1229,69 @@ async fn handle_pegin_request_default( Ok(()) } +async fn defer_confirm_instance_until_previous_graph_presigned( + ctx: &mut HandlerContext<'_>, + instance_id: Uuid, + successor_nonce: u64, + operator_pubkey: &PublicKey, +) -> Result { + if successor_nonce == 0 { + return Ok(false); + } + + let previous_nonce = successor_nonce - 1; + let retry_message = GOATMessage::new( + Actor::Operator, + GOATMessageContent::ConfirmInstance(ConfirmInstance { instance_id }), + ); + let Some((previous_instance_id, previous_graph_id)) = + get_graph_id_by_nonce(ctx.local_db, previous_nonce, operator_pubkey).await? + else { + push_local_unhandled_messages(ctx.local_db, instance_id, &retry_message, 60).await?; + tracing::warn!( + "Defer ConfirmInstance for {instance_id}: previous graph with nonce {previous_nonce} is not available locally" + ); + return Ok(true); + }; + let Some(previous_graph) = + get_graph(ctx.local_db, previous_instance_id, previous_graph_id).await? + else { + if let Err(error) = try_send_sync_graph_request( + ctx.swarm, + ctx.goat_client, + previous_instance_id, + previous_graph_id, + ) + .await + { + tracing::warn!( + "Failed to send SyncGraphRequest for previous graph {previous_instance_id}:{previous_graph_id}: {error}" + ); + } + push_local_unhandled_messages(ctx.local_db, instance_id, &retry_message, 60).await?; + tracing::info!( + "Defer ConfirmInstance for {instance_id}: waiting for previous graph raw data {previous_instance_id}:{previous_graph_id}" + ); + return Ok(true); + }; + if previous_graph.committee_pre_signed() { + return Ok(false); + } + + push_local_unhandled_messages(ctx.local_db, instance_id, &retry_message, 60).await?; + let message_content = GOATMessageContent::CreateGraph(CreateGraph { + instance_id: previous_instance_id, + graph_id: previous_graph_id, + graph_nonce: previous_graph.parameters.graph_nonce, + graph: previous_graph, + }); + send_to_peer(ctx.swarm, GOATMessage::new(Actor::All, message_content)).await?; + tracing::info!( + "Defer ConfirmInstance for {instance_id}: re-broadcast previous CreateGraph {previous_instance_id}:{previous_graph_id} until it is committee pre-signed" + ); + Ok(true) +} + #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id))] async fn handle_confirm_instance_operator( ctx: &mut HandlerContext<'_>, @@ -1198,6 +1308,16 @@ async fn handle_confirm_instance_operator( ) .await? { + if defer_confirm_instance_until_previous_graph_presigned( + ctx, + instance_id, + graph.parameters.graph_nonce, + &local_operator_pubkey, + ) + .await? + { + return Ok(()); + } let graph_id = graph.parameters.graph_id; tracing::info!("Graph already created for {instance_id}, graph_id: {}", graph_id); let message_content = GOATMessageContent::CreateGraph(CreateGraph { @@ -1222,6 +1342,18 @@ async fn handle_confirm_instance_operator( .map(|pending| pending.graph_id) }; if let Some(graph_id) = pending_graph_id { + if let Some((next_graph_nonce, _)) = + get_current_prekickoff_tx(ctx.local_db, &local_operator_pubkey).await? + && defer_confirm_instance_until_previous_graph_presigned( + ctx, + instance_id, + next_graph_nonce, + &local_operator_pubkey, + ) + .await? + { + return Ok(()); + } tracing::info!("Resume pending graph setup for {instance_id}, graph_id: {graph_id}"); let message_content = GOATMessageContent::InitGraph(InitGraph { instance_id, graph_id }); send_to_peer(ctx.swarm, GOATMessage::new(Actor::Verifier, message_content)).await?; @@ -1246,6 +1378,19 @@ async fn handle_confirm_instance_operator( ); bail!("Invalid ConfirmInstance: pegin deposit tx {pegin_deposit_txid} not found on chain"); } + + if let Some((next_graph_nonce, _)) = + get_current_prekickoff_tx(ctx.local_db, &local_operator_pubkey).await? + && defer_confirm_instance_until_previous_graph_presigned( + ctx, + instance_id, + next_graph_nonce, + &local_operator_pubkey, + ) + .await? + { + return Ok(()); + } // after PeginPrepare is confirmed, broadcast InitGraph and let Verifiers generate GC. // 2. save the instance data to local db @@ -1682,7 +1827,7 @@ fn decode_soldering_proof_payload( verifier_index, actual_len = payload.len(), total_len, - payload_hash = %soldering_payload_hash_hex(&payload_hash), + payload_hash = %soldering_payload_hash_hex(payload_hash), "SolderingProof payload length mismatch" ); bail!( @@ -1696,7 +1841,7 @@ fn decode_soldering_proof_payload( instance_id = %instance_id, graph_id = %graph_id, verifier_index, - expected_hash = %soldering_payload_hash_hex(&payload_hash), + expected_hash = %soldering_payload_hash_hex(payload_hash), actual_hash = %soldering_payload_hash_hex(&actual_hash), "SolderingProof payload hash mismatch" ); @@ -1843,7 +1988,7 @@ async fn handle_compact_soldering_proof_operator( operator_pre_sign(operator_master_key.master_keypair(), &mut graph)?; let graph = graph.to_simplified()?; - store_graph(ctx.local_db, &graph).await?; + store_operator_presigned_graph(ctx.local_db, &graph).await?; let mut storage = ctx.local_db.acquire().await?; storage.delete_pending_graph_init(&instance_id, &local_operator_pubkey.to_string()).await?; @@ -1888,8 +2033,21 @@ async fn handle_create_graph_verifier( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, + graph_nonce: u64, graph: &SimplifiedBitvmGcGraph, ) -> Result<()> { + if !message_identity_matches( + "CreateGraph", + instance_id, + graph_id, + Some(graph_nonce), + graph.parameters.instance_parameters.instance_id, + graph.parameters.graph_id, + graph.parameters.graph_nonce, + ) { + return Ok(()); + } + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); let local_verifier_pubkey: PublicKey = verifier_master_key.master_keypair().public_key().into(); let Some(verifier_index) = @@ -2122,9 +2280,22 @@ async fn handle_create_graph_committee( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, + graph_nonce: u64, graph: &SimplifiedBitvmGcGraph, content: &GOATMessageContent, ) -> Result<()> { + if !message_identity_matches( + "CreateGraph", + instance_id, + graph_id, + Some(graph_nonce), + graph.parameters.instance_parameters.instance_id, + graph.parameters.graph_id, + graph.parameters.graph_nonce, + ) { + return Ok(()); + } + // received from Operator if graph.parameters.graph_nonce > 0 { let previous_nonce = graph.parameters.graph_nonce - 1; @@ -2132,8 +2303,9 @@ async fn handle_create_graph_committee( .await? { Some((previous_instance_id, previous_graph_id)) => { - if get_graph(ctx.local_db, previous_instance_id, previous_graph_id).await?.is_none() - { + let Some(previous_graph) = + get_graph(ctx.local_db, previous_instance_id, previous_graph_id).await? + else { if let Err(e) = try_send_sync_graph_request( ctx.swarm, ctx.goat_client, @@ -2152,6 +2324,14 @@ async fn handle_create_graph_committee( "Defer CreateGraph for {instance_id}:{graph_id}: waiting for previous graph raw data {previous_instance_id}:{previous_graph_id}" ); return Ok(()); + }; + if !previous_graph.committee_pre_signed() { + let message = make_message(ctx, content); + push_local_unhandled_messages(ctx.local_db, graph_id, &message, 60).await?; + tracing::info!( + "Defer CreateGraph for {instance_id}:{graph_id}: waiting for previous graph {previous_instance_id}:{previous_graph_id} to be committee pre-signed" + ); + return Ok(()); } } None => { @@ -2176,7 +2356,7 @@ async fn handle_create_graph_committee( bail!(e) }; // 2. save the graph data to local db - store_graph(ctx.local_db, graph).await?; + store_operator_presigned_graph(ctx.local_db, graph).await?; // 3. start committee setup once verifier params endorsements are complete try_start_graph_committee_setup(ctx, instance_id, graph_id, graph).await } @@ -2249,7 +2429,7 @@ async fn handle_verifier_graph_params_endorsement_committee( graph_id, *verifier_pubkey, verifier_index, - signature.clone(), + *signature, ) .await?; try_start_graph_committee_setup(ctx, instance_id, graph_id, &graph).await @@ -2478,7 +2658,7 @@ async fn handle_nonce_generation_operator( // 3. if received enough endorsement signatures, mark the graph as endorsed, send the graph to local db, broadcast GraphFinalize // Operator may receive EndorseGraph, CommitteePresign or NonceGeneration messages in any order // So we need to check if we have collected enough endorsements, pub_nonces and partial_sigs every time we receive them - try_finalize_graph( + if let Some((finalized_graph, _)) = try_finalize_graph( ctx.swarm, ctx.local_db, ctx.goat_client, @@ -2487,7 +2667,10 @@ async fn handle_nonce_generation_operator( Some(&graph), true, ) - .await?; + .await? + { + refresh_newly_finalized_graph(ctx, instance_id, graph_id, &finalized_graph).await?; + } Ok(()) } @@ -2700,8 +2883,19 @@ async fn handle_committee_presign_operator( // 3. if received enough endorsement signatures, mark the graph as endorsed, send the graph to local database, broadcast GraphFinalize // Operator may receive EndorseGraph, CommitteePresign or NonceGeneration messages in any order // So we need to check if we have collected enough endorsements, pub_nonces and partial_sigs every time we receive them - try_finalize_graph(ctx.swarm, ctx.local_db, ctx.goat_client, instance_id, graph_id, None, true) - .await?; + if let Some((finalized_graph, _)) = try_finalize_graph( + ctx.swarm, + ctx.local_db, + ctx.goat_client, + instance_id, + graph_id, + None, + true, + ) + .await? + { + refresh_newly_finalized_graph(ctx, instance_id, graph_id, &finalized_graph).await?; + } Ok(()) } @@ -2789,7 +2983,7 @@ async fn handle_endorse_graph_operator( // 3. if received enough endorsement signatures, mark the graph as endorsed, send the graph to local database, broadcast GraphFinalize // Operator may receive EndorseGraph, CommitteePresign or NonceGeneration messages in any order // So we need to check if we have collected enough endorsements, pub_nonces and partial_sigs every time we receive them - try_finalize_graph( + if let Some((finalized_graph, _)) = try_finalize_graph( ctx.swarm, ctx.local_db, ctx.goat_client, @@ -2798,7 +2992,10 @@ async fn handle_endorse_graph_operator( Some(&graph), true, ) - .await?; + .await? + { + refresh_newly_finalized_graph(ctx, instance_id, graph_id, &finalized_graph).await?; + } Ok(()) } @@ -2807,10 +3004,23 @@ async fn handle_graph_finalize_committee( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, + graph_nonce: u64, graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], params_endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], ) -> Result<()> { + if !message_identity_matches( + "GraphFinalize", + instance_id, + graph_id, + Some(graph_nonce), + graph.parameters.instance_parameters.instance_id, + graph.parameters.graph_id, + graph.parameters.graph_nonce, + ) { + return Ok(()); + } + // received from Operator // 1. check graph data if let Err(e) = todo_funcs::validate_finalized_graph( @@ -2833,8 +3043,8 @@ async fn handle_graph_finalize_committee( } bail!(e) } - // 2. save the graph data to local db - store_graph(ctx.local_db, graph).await?; + // 2. Store a finalized graph only when it upgrades the local graph. + let _ = store_finalized_graph_if_needed(ctx.local_db, graph).await?; store_committee_endorsements_for_graph( ctx.local_db, instance_id, @@ -2843,12 +3053,13 @@ async fn handle_graph_finalize_committee( params_endorse_sigs.to_owned(), ) .await?; - // After storing, mark the graph as endorsed + // After storing, mark the graph as finalized for the instance threshold. mark_graph_as_endorsed(ctx.local_db, instance_id, graph_id).await?; + try_transition_instance_to_presigned(ctx.local_db, instance_id).await?; + let finalized_graph = BitvmGcGraph::from_simplified(graph)?; + refresh_newly_finalized_graph(ctx, instance_id, graph_id, &finalized_graph).await?; // 3. if endorsed graph count >= threshold, generate & broadcast PeginConfirmNonce - if get_endorsed_graph_count(ctx.local_db, instance_id).await? - >= todo_funcs::min_required_operator() - { + if has_required_presigned_graphs(ctx.local_db, instance_id).await? { let committee_master_key = CommitteeMasterKey::new(get_bitvm_key()?); let instance_keypair = load_committee_instance_keypair(&committee_master_key, instance_id)?; let local_committee_pubkey = instance_keypair.public_key().into(); @@ -2924,10 +3135,23 @@ async fn handle_graph_finalize_default( ctx: &mut HandlerContext<'_>, instance_id: Uuid, graph_id: Uuid, + graph_nonce: u64, graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], params_endorse_sigs: &[(PublicKey, alloy::primitives::Address, Vec)], ) -> Result<()> { + if !message_identity_matches( + "GraphFinalize", + instance_id, + graph_id, + Some(graph_nonce), + graph.parameters.instance_parameters.instance_id, + graph.parameters.graph_id, + graph.parameters.graph_nonce, + ) { + return Ok(()); + } + // received from Operator // 1. check graph data if let Err(e) = todo_funcs::validate_finalized_graph( @@ -2950,8 +3174,20 @@ async fn handle_graph_finalize_default( } bail!(e) } - // 2. save the graph data to local db - store_graph(ctx.local_db, graph).await?; + // 2. Store a finalized graph only when it upgrades the local graph. + let _ = store_finalized_graph_if_needed(ctx.local_db, graph).await?; + store_committee_endorsements_for_graph( + ctx.local_db, + instance_id, + graph_id, + endorse_sigs.to_owned(), + params_endorse_sigs.to_owned(), + ) + .await?; + mark_graph_as_endorsed(ctx.local_db, instance_id, graph_id).await?; + try_transition_instance_to_presigned(ctx.local_db, instance_id).await?; + let finalized_graph = BitvmGcGraph::from_simplified(graph)?; + refresh_newly_finalized_graph(ctx, instance_id, graph_id, &finalized_graph).await?; Ok(()) } @@ -3421,35 +3657,33 @@ async fn handle_kickoff_ready_operator( None => return Ok(()), }; let mut current_graph = BitvmGcGraph::from_simplified(¤t_graph)?; - let current_graph_start_status = - get_graph_status(ctx.local_db, current_instance_id, current_graph_id) - .await? - .ok_or_else(|| { - anyhow!("Graph status not found for {current_instance_id}:{current_graph_id}") - })?; - let (current_graph_status, _current_graph_sub_status, current_graph_scan) = refresh_graph( + let current_graph_refresh = refresh_graph( ctx.local_db, ctx.btc_client, ctx.goat_client, current_instance_id, current_graph_id, - Some(¤t_graph), - Some(current_graph_start_status), - None, - ) - .await?; - compensate_graph_events( - ctx.local_db, - ctx.btc_client, - current_instance_id, - current_graph_id, - Some(¤t_graph), - current_graph_scan.as_ref(), - Some(current_graph_start_status), - current_graph_start_status, - current_graph_status, + ¤t_graph, ) .await?; + let current_graph_status = current_graph_refresh.status; + if current_graph_refresh.status_transition_accepted { + compensate_graph_events( + ctx.local_db, + ctx.btc_client, + current_instance_id, + current_graph_id, + ¤t_graph, + current_graph_refresh.scan.as_ref(), + // This is a recovery scan, not a transition from the local + // projection. Start from the protocol baseline so a previous + // crash between status persistence and message enqueue can be + // repaired by idempotent message upserts. + GraphStatus::OperatorPresigned, + current_graph_status, + ) + .await?; + } if current_graph_status.is_closed() { continue; } else if current_graph_status.is_pegout_started() { @@ -3732,18 +3966,24 @@ async fn handle_previous_graph_after_prekickoff( None => return Ok(()), }; let prev_graph = BitvmGcGraph::from_simplified(&prev_graph)?; - let prev_graph_start_status = get_graph_status(ctx.local_db, prev_instance_id, prev_graph_id) - .await? - .ok_or_else(|| anyhow!("Graph status not found for {prev_instance_id}:{prev_graph_id}"))?; - let (prev_graph_status, _prev_graph_sub_status) = refresh_and_compensate( - ctx, - prev_instance_id, - prev_graph_id, - Some(&prev_graph), - Some(prev_graph_start_status), - prev_graph_start_status, - ) - .await?; + let (prev_graph_status, _prev_graph_sub_status, status_transition_accepted) = + refresh_and_compensate( + ctx, + prev_instance_id, + prev_graph_id, + &prev_graph, + // Do not use the persisted status as a compensation anchor: it + // may have been committed just before a crash. Idempotent message + // upserts make a baseline recovery scan safe to retry. + GraphStatus::OperatorPresigned, + ) + .await?; + if !status_transition_accepted { + tracing::warn!( + "Ignore prekickoff follow-up for {instance_id}:{graph_id}: previous graph status scan was rejected" + ); + return Ok(()); + } if !tx_on_chain(ctx.btc_client, &prev_graph.kickoff.tx().compute_txid()).await? { verifier_force_skip_kickoff(ctx.btc_client, &prev_graph).await?; } else if !prev_graph_status.is_closed() { @@ -4559,93 +4799,83 @@ async fn handle_assert_sent_verifier( let outpoint = connector_e_input.outpoint; if let Some(commit_pubin_txid) = outpoint_spent_txid(ctx.btc_client, &outpoint.txid, outpoint.vout as u64).await? + && let Some(commit_pubin_tx) = ctx.btc_client.get_tx(&commit_pubin_txid).await? + && let Some(commit_pubin_txin) = commit_pubin_tx.input.first() { - if let Some(commit_pubin_tx) = ctx.btc_client.get_tx(&commit_pubin_txid).await? { - if let Some(commit_pubin_txin) = commit_pubin_tx.input.first() { - let assert_txin = assert_tx - .input - .first() - .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; - let wci_txid = graph.watchtower_challenge_init.tx().compute_txid(); - let watchtower_timeout_txids = graph - .watchtower_challenge_timeouts - .iter() - .map(|tx| tx.tx().compute_txid()) - .collect::>(); - let ack_txins = match collect_ack_txins( - ctx.btc_client, - &wci_txid, - &watchtower_timeout_txids, + let assert_txin = assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; + let wci_txid = graph.watchtower_challenge_init.tx().compute_txid(); + let watchtower_timeout_txids = graph + .watchtower_challenge_timeouts + .iter() + .map(|tx| tx.tx().compute_txid()) + .collect::>(); + let ack_txins = match collect_ack_txins( + ctx.btc_client, + &wci_txid, + &watchtower_timeout_txids, + ) + .await + { + Ok(txins) => txins, + Err(e) => { + let delay_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()); + let message = make_message(ctx, content); + push_local_unhandled_messages( + ctx.local_db, + graph_id, + &message, + delay_secs as usize, ) - .await - { - Ok(txins) => txins, - Err(e) => { - let delay_secs = - todo_funcs::avg_block_time_secs(ctx.btc_client.network()); - let message = make_message(ctx, content); - push_local_unhandled_messages( - ctx.local_db, - graph_id, - &message, - delay_secs as usize, - ) - .await?; - tracing::info!( - "Retry AssertSent later for {instance_id}:{graph_id}: ACK inputs are not ready: {e}" - ); - return Ok(()); - } + .await?; + tracing::info!( + "Retry AssertSent later for {instance_id}:{graph_id}: ACK inputs are not ready: {e}" + ); + return Ok(()); + } + }; + match validate_pubin_disprove(&graph, commit_pubin_txin, assert_txin, &ack_txins) { + Ok(Some((witness_data, _))) => { + // build pubin disprove Tx + let pubin_disprove_tx_total_input_amount = graph + .operator_assert + .connector_d_input() + .map_err(|e| anyhow!("failed to get connector-d input: {e}"))? + .amount; + let pubin_disprove_txin = build_pubin_disprove_txin(&graph, witness_data)?; + let pubin_disprove_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![pubin_disprove_txin], + output: vec![goat::scripts::p2a_output()], }; - match validate_pubin_disprove( - &graph, - commit_pubin_txin, - assert_txin, - &ack_txins, - ) { - Ok(Some((witness_data, _))) => { - // build pubin disprove Tx - let pubin_disprove_tx_total_input_amount = graph - .operator_assert - .connector_d_input() - .map_err(|e| anyhow!("failed to get connector-d input: {e}"))? - .amount; - let pubin_disprove_txin = - build_pubin_disprove_txin(&graph, witness_data)?; - let pubin_disprove_tx = bitcoin::Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![pubin_disprove_txin], - output: vec![goat::scripts::p2a_output()], - }; - broadcast_tx_with_cpfp( - ctx.btc_client, - pubin_disprove_tx, - pubin_disprove_tx_total_input_amount, - ) - .await?; - return Ok(()); - } - Ok(None) => tracing::debug!( - "PubinDisprove invalid for {instance_id}:{graph_id}: operator pubin consistent, proceeding to ChallengeAssert" - ), - Err(e) => { - let delay_secs = - todo_funcs::avg_block_time_secs(ctx.btc_client.network()); - let message = make_message(ctx, content); - push_local_unhandled_messages( - ctx.local_db, - graph_id, - &message, - delay_secs as usize, - ) - .await?; - tracing::warn!( - "Retry AssertSent later for {instance_id}:{graph_id}: PubinDisprove check failed: {e}" - ); - return Ok(()); - } - } + broadcast_tx_with_cpfp( + ctx.btc_client, + pubin_disprove_tx, + pubin_disprove_tx_total_input_amount, + ) + .await?; + return Ok(()); + } + Ok(None) => tracing::debug!( + "PubinDisprove invalid for {instance_id}:{graph_id}: operator pubin consistent, proceeding to ChallengeAssert" + ), + Err(e) => { + let delay_secs = todo_funcs::avg_block_time_secs(ctx.btc_client.network()); + let message = make_message(ctx, content); + push_local_unhandled_messages( + ctx.local_db, + graph_id, + &message, + delay_secs as usize, + ) + .await?; + tracing::warn!( + "Retry AssertSent later for {instance_id}:{graph_id}: PubinDisprove check failed: {e}" + ); + return Ok(()); } } } @@ -4667,25 +4897,82 @@ async fn handle_assert_sent_verifier( return Ok(()); } - let Some(saved_verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? - .and_then(|state| state.verifier) + let strict = false; + broadcast_verifier_challenge_assert_tx( + ctx.local_db, + ctx.btc_client, + &graph, + instance_id, + graph_id, + &assert_tx, + strict, + ) + .await?; + + Ok(()) +} + +pub async fn broadcast_verifier_challenge_assert_tx( + local_db: &LocalDB, + btc_client: &BTCClient, + graph: &BitvmGcGraph, + instance_id: Uuid, + graph_id: Uuid, + assert_tx: &bitcoin::Transaction, + strict: bool, +) -> Result> { + let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); + let verifier_pubkey = verifier_master_key.master_keypair().public_key().into(); + let Some(verifier_index) = + find_verifier_index_by_pubkey(&graph.parameters.gc_data, &verifier_pubkey)? + else { + if strict { + bail!("local verifier has no graph slot for {instance_id}:{graph_id}"); + } + tracing::debug!( + "Ignore AssertSent for {instance_id}:{graph_id}: local verifier has no graph slot" + ); + return Ok(None); + }; + validate_verifier_slot(graph, verifier_index)?; + + let assert_txin = assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; + let assert_witness = extract_operator_assert_witness_for_challenge(graph, assert_txin) + .map_err(|e| anyhow!("failed to extract operator assert witness: {e}"))?; + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for operator assert")?; + let static_input = derive_operator_static_input()?; + + let Some(saved_verifier_state) = + load_babe_setup_state(local_db, instance_id, graph_id)?.and_then(|state| state.verifier) else { + if strict { + bail!("missing BABE verifier setup state for {instance_id}:{graph_id}"); + } tracing::warn!( "Ignore AssertSent for {instance_id}:{graph_id}: missing BABE verifier setup state" ); - return Ok(()); + return Ok(None); }; let Some(soldering_proof_ready) = saved_verifier_state.soldering_proof_ready.as_ref() else { + if strict { + bail!("missing soldering proof reference for {instance_id}:{graph_id}"); + } tracing::warn!( "Ignore AssertSent for {instance_id}:{graph_id}: missing soldering proof reference" ); - return Ok(()); + return Ok(None); }; if soldering_proof_ready.verifier_index != verifier_index { + if strict { + bail!("local setup slot does not match graph owner slot for {instance_id}:{graph_id}"); + } tracing::warn!( "Ignore AssertSent for {instance_id}:{graph_id}: local setup slot does not match graph owner slot" ); - return Ok(()); + return Ok(None); } let challenge_witness = build_real_challenge_assert_witness( &saved_verifier_state.private_state, @@ -4716,17 +5003,20 @@ async fn handle_assert_sent_verifier( .cloned() .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; let challenge_assert_tx = - build_verifier_assert_tx(&graph, operator_assert_txin, verifier_index, labels)?; + build_verifier_assert_tx(graph, operator_assert_txin, verifier_index, labels)?; + let challenge_assert_txid = challenge_assert_tx.compute_txid(); + if btc_client.get_tx(&challenge_assert_txid).await?.is_some() { + tracing::info!( + "Verifier ChallengeAssert already exists for {instance_id}:{graph_id}: verifier_index={verifier_index}, txid={challenge_assert_txid}" + ); + return Ok(Some((challenge_assert_txid, verifier_index))); + } let challenge_assert_tx_total_input_amount = graph.verifier_asserts[verifier_index].prev_outs().iter().map(|o| o.value).sum::(); - broadcast_tx_with_cpfp( - ctx.btc_client, - challenge_assert_tx, - challenge_assert_tx_total_input_amount, - ) - .await?; + broadcast_tx_with_cpfp(btc_client, challenge_assert_tx, challenge_assert_tx_total_input_amount) + .await?; - Ok(()) + Ok(Some((challenge_assert_txid, verifier_index))) } // compute msg after ChallengeAssert is broadcast and broadcast WronglyChallenge transaction. @@ -4849,23 +5139,37 @@ async fn handle_challenge_assert_sent_operator( vk, dyn_pubin, )?; - let (wrongly_challenged_input, _amount) = operator_sign_wrongly_challenged( + let (wrongly_challenged_input, input_amount) = operator_sign_wrongly_challenged( &graph, verifier_index, &wrongly_challenged_witness.final_msg, )?; - let wrongly_challenged_tx = bitcoin::Transaction { - version: bitcoin::transaction::Version(2), - lock_time: bitcoin::absolute::LockTime::ZERO, - input: vec![wrongly_challenged_input], - output: vec![goat::scripts::p2a_output()], - }; - broadcast_tx(ctx.btc_client, &wrongly_challenged_tx).await?; + let operator_keypair = OperatorMasterKey::new(get_bitvm_key()?).master_keypair(); + build_sign_and_broadcast_tx( + ctx.btc_client, + operator_keypair, + vec![wrongly_challenged_input], + input_amount, + vec![ + goat::scripts::p2a_output(), + // This makes the non-witness transaction size safely exceed the + // relay minimum even before the locally funded fee input is added. + bitcoin::TxOut { + value: Amount::ZERO, + script_pubkey: goat::scripts::generate_opreturn_script( + WRONGLY_CHALLENGED_OP_RETURN_DATA.to_vec(), + ), + }, + ], + ) + .await?; Ok(()) } +const WRONGLY_CHALLENGED_OP_RETURN_DATA: &[u8] = b"wrongly-challenged"; + // broadcast NoWithdraw after the ChallengeAssert timelock expires. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_wrongly_challenge_timeout_verifier( @@ -5461,13 +5765,6 @@ async fn handle_sync_graph( graph: &SimplifiedBitvmGcGraph, ) -> Result<()> { // sent by relayer nodes in response to SyncGraphRequest - if graph_exists(ctx.local_db, instance_id, graph_id).await? { - tracing::warn!( - "Ignore SyncGraph for {instance_id}:{graph_id}: graph already exists locally" - ); - return Ok(()); - } - if !ctx .goat_client .committee_mana_is_validate_peer_id(&ctx.from_peer_id.to_bytes()) @@ -5486,12 +5783,15 @@ async fn handle_sync_graph( return Ok(()); } - if graph.parameters.instance_parameters.instance_id != instance_id - || graph.parameters.graph_id != graph_id - { - tracing::warn!( - "Ignore SyncGraph for {instance_id}:{graph_id}: message identifiers do not match graph parameters" - ); + if !message_identity_matches( + "SyncGraph", + instance_id, + graph_id, + None, + graph.parameters.instance_parameters.instance_id, + graph.parameters.graph_id, + graph.parameters.graph_nonce, + ) { return Ok(()); } @@ -5535,16 +5835,9 @@ async fn handle_sync_graph( return Ok(()); } let simplified_graph = graph.to_simplified()?; - store_graph(ctx.local_db, &simplified_graph).await?; - refresh_and_compensate( - ctx, - instance_id, - graph_id, - Some(&graph), - None, - GraphStatus::OperatorPresigned, - ) - .await?; + let _ = store_finalized_graph_if_needed(ctx.local_db, &simplified_graph).await?; + refresh_and_compensate(ctx, instance_id, graph_id, &graph, GraphStatus::OperatorPresigned) + .await?; Ok(()) } @@ -5792,8 +6085,8 @@ mod tests { for label in labels { witness.push(label.as_slice()); } - witness.push(&[0xde, 0xad]); // placeholder script - witness.push(&[0xbe, 0xef]); // placeholder control block + witness.push([0xde, 0xad]); // placeholder script + witness.push([0xbe, 0xef]); // placeholder control block bitcoin::Transaction { version: bitcoin::transaction::Version(2), lock_time: bitcoin::absolute::LockTime::ZERO, @@ -5856,4 +6149,29 @@ mod tests { }; assert!(recover_challenge_assert_witness(&empty_tx, 0).is_err()); } + + #[test] + fn wrongly_challenge_outputs_exceed_minimum_non_witness_size() { + // Bitcoin Core rejects standard transactions smaller than 65 non-witness bytes. + let tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn::default()], + output: vec![ + goat::scripts::p2a_output(), + bitcoin::TxOut { + value: Amount::ZERO, + script_pubkey: goat::scripts::generate_opreturn_script( + WRONGLY_CHALLENGED_OP_RETURN_DATA.to_vec(), + ), + }, + ], + }; + + assert!( + tx.base_size() >= 65, + "wrongly-challenge transaction is {} non-witness bytes", + tx.base_size() + ); + } } diff --git a/node/src/main.rs b/node/src/main.rs index 4a87fb48..1166376e 100644 --- a/node/src/main.rs +++ b/node/src/main.rs @@ -13,6 +13,8 @@ use libp2p::PeerId; use libp2p_metrics::Registry; use std::error::Error; use std::sync::{Arc, Mutex}; +use std::time::Instant; +use tracing::Instrument; use tracing_subscriber::EnvFilter; use bitvm_noded::utils::{ @@ -121,7 +123,13 @@ async fn main() -> Result<(), Box> { } return Ok(()); } - let _ = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).try_init(); + let _ = tracing_subscriber::fmt() + .json() + .flatten_event(true) + .with_current_span(true) + .with_span_list(true) + .with_env_filter(EnvFilter::from_default_env()) + .try_init(); validate_soldering_proof_payload_store_config(&actor)?; let is_publisher = actor == Actor::Publisher || actor == Actor::All; @@ -138,6 +146,7 @@ async fn main() -> Result<(), Box> { // Create cancellation token for graceful shutdown let cancellation_token = CancellationToken::new(); let mut task_handles: Vec>> = vec![]; + let mut task_names: Vec<&'static str> = vec![]; // init bitvmswarm let bitvm_network_manager = BitvmNetworkManager::new( BitvmSwarmConfig { @@ -157,6 +166,16 @@ async fn main() -> Result<(), Box> { &mut metric_registry, )?; let peer_id_string = bitvm_network_manager.get_peer_id_string(); + let bitcoin_network = get_network().to_string(); + let node_span = tracing::info_span!( + "node", + service = "bitvm-noded", + role = %actor, + peer_id = %peer_id_string, + bitcoin_network = %bitcoin_network, + version = env!("CARGO_PKG_VERSION"), + ); + let _node_span_guard = node_span.enter(); let local_db = store::create_local_db(&opt.db_path).await; let handler = BitvmNodeProcessor { local_db: local_db.clone(), @@ -167,6 +186,17 @@ async fn main() -> Result<(), Box> { .then(|| Arc::new(BabeBundleBuilder::new())), }; + tracing::info!( + event = "service_started", + service = "bitvm-noded", + role = %actor, + peer_id = %peer_id_string, + bitcoin_network = ?get_network(), + version = env!("CARGO_PKG_VERSION"), + rpc_addr = %opt.rpc_addr, + "node initialization completed" + ); + let actor_clone1 = actor.clone(); let actor_clone2 = actor.clone(); let actor_clone3 = actor.clone(); @@ -188,48 +218,76 @@ async fn main() -> Result<(), Box> { // Spawn RPC service task with cancellation support let cancel_token_clone = cancellation_token.clone(); - task_handles.push(tokio::spawn(async move { - match rpc_service::serve( - opt_rpc_addr, - local_db_clone1, - actor_clone1, - peer_id_string_clone, - metric_registry_clone, - cancel_token_clone, - ) - .await - { - Ok(tag) => Ok(tag), - Err(e) => { - tracing::error!("RPC service error: {}", e); - Err("rpc_error".to_string()) + let rpc_task_span = node_span.clone(); + task_handles.push(tokio::spawn( + async move { + match rpc_service::serve( + opt_rpc_addr, + local_db_clone1, + actor_clone1, + peer_id_string_clone, + metric_registry_clone, + cancel_token_clone, + ) + .await + { + Ok(tag) => Ok(tag), + Err(error) => { + tracing::error!( + event = "core_task_wrapper_error", + service = "bitvm-noded", + task = "rpc_service", + outcome = "failed", + error_class = "rpc", + error = %error, + "RPC service task exited with an error" + ); + Err("rpc_error".to_string()) + } } } - })); + .instrument(rpc_task_span), + )); + task_names.push("rpc_service"); // if actor == Actor::Committee || actor == Actor::Operator { let cancel_token_clone = cancellation_token.clone(); - task_handles.push(tokio::spawn(async move { - let goat_init_config = goat_config_from_env().await; - let goat_client = Arc::new(GOATClient::new(goat_init_config.clone(), get_goat_network())); - let btc_client = Arc::new(BTCClient::new(get_network(), get_btc_url_from_env().as_deref())); - match run_watch_event_task( - actor_clone2, - local_db_clone2, - btc_client, - goat_client, - 5, - cancel_token_clone, - goat_init_config, - ) - .await - { - Ok(tag) => Ok(tag), - Err(e) => { - tracing::error!("Watch event task error: {}", e); - Err("watch_error".to_string()) + let event_watcher_task_span = node_span.clone(); + task_handles.push(tokio::spawn( + async move { + let goat_init_config = goat_config_from_env().await; + let goat_client = + Arc::new(GOATClient::new(goat_init_config.clone(), get_goat_network())); + let btc_client = + Arc::new(BTCClient::new(get_network(), get_btc_url_from_env().as_deref())); + match run_watch_event_task( + actor_clone2, + local_db_clone2, + btc_client, + goat_client, + 5, + cancel_token_clone, + goat_init_config, + ) + .await + { + Ok(tag) => Ok(tag), + Err(error) => { + tracing::error!( + event = "core_task_wrapper_error", + service = "bitvm-noded", + task = "event_watcher", + outcome = "failed", + error_class = "watcher", + error = %error, + "event watcher task exited with an error" + ); + Err("watch_error".to_string()) + } } } - })); + .instrument(event_watcher_task_span), + )); + task_names.push("event_watcher"); if is_publisher { let start_cosmos_block = sequencer_set_monitor_start_cosmos_block.unwrap(); @@ -249,85 +307,194 @@ async fn main() -> Result<(), Box> { .await { Ok(tag) => Ok(tag), - Err(e) => { - tracing::error!("Sequencer set monitor task error: {}", e); + Err(error) => { + tracing::error!("Sequencer set monitor task error: {}", error); Err("sequencer_set_monitor_error".to_string()) } } })); + task_names.push("sequencer_set_monitor"); } // } let cancel_token_clone = cancellation_token.clone(); - task_handles.push(tokio::spawn(async move { - let goat_client = - Arc::new(GOATClient::new(goat_config_from_env().await, get_goat_network())); - let btc_client = Arc::new(BTCClient::new(get_network(), get_btc_url_from_env().as_deref())); - match run_maintenance_tasks( - actor_clone3, - local_db_clone3, - btc_client, - goat_client, - 10, - cancel_token_clone, - ) - .await - { - Ok(tag) => Ok(tag), - Err(e) => { - tracing::error!("Maintenance task error: {}", e); - Err("maintenance_error".to_string()) + let maintenance_task_span = node_span.clone(); + task_handles.push(tokio::spawn( + async move { + let goat_client = + Arc::new(GOATClient::new(goat_config_from_env().await, get_goat_network())); + let btc_client = + Arc::new(BTCClient::new(get_network(), get_btc_url_from_env().as_deref())); + match run_maintenance_tasks( + actor_clone3, + local_db_clone3, + btc_client, + goat_client, + 10, + cancel_token_clone, + ) + .await + { + Ok(tag) => Ok(tag), + Err(error) => { + tracing::error!( + event = "core_task_wrapper_error", + service = "bitvm-noded", + task = "maintenance", + outcome = "failed", + error_class = "maintenance", + error = %error, + "maintenance task exited with an error" + ); + Err("maintenance_error".to_string()) + } } } - })); + .instrument(maintenance_task_span), + )); + task_names.push("maintenance"); let swarm_actor = actor.clone(); let cancel_token_clone = cancellation_token.clone(); - task_handles.push(tokio::spawn(async move { - let result = tokio::task::spawn_blocking(move || { - let rt = tokio::runtime::Handle::current(); - rt.block_on(async { - start_handle_swarm_msg_task( - swarm_actor, - bitvm_network_manager, - handler, - cancel_token_clone, - ) - .await + let p2p_task_span = node_span.clone(); + let p2p_blocking_span = p2p_task_span.clone(); + task_handles.push(tokio::spawn( + async move { + let result = tokio::task::spawn_blocking(move || { + let _span_guard = p2p_blocking_span.enter(); + let rt = tokio::runtime::Handle::current(); + rt.block_on(async { + start_handle_swarm_msg_task( + swarm_actor, + bitvm_network_manager, + handler, + cancel_token_clone, + ) + .await + }) }) - }) - .await; - match result { - Ok(tag) => Ok(tag), - Err(e) => { - tracing::error!("Swarm task spawn error: {}", e); - Err("swarm_spawn_error".to_string()) + .await; + match result { + Ok(Ok(tag)) => Ok(tag), + Ok(Err(error)) => { + tracing::error!( + event = "core_task_wrapper_error", + service = "bitvm-noded", + task = "p2p_swarm", + outcome = "failed", + error_class = "p2p", + error = %error, + "p2p swarm task exited with an error" + ); + Err("swarm_error".to_string()) + } + Err(error) => { + tracing::error!( + event = "core_task_wrapper_error", + service = "bitvm-noded", + task = "p2p_swarm", + outcome = "failed", + error_class = "join", + error = %error, + "p2p swarm blocking task failed to join" + ); + Err("swarm_spawn_error".to_string()) + } + } + } + .instrument(p2p_task_span), + )); + task_names.push("p2p_swarm"); + + let heartbeat_actor = actor.clone(); + let heartbeat_peer_id = peer_id_string.clone(); + let cancel_token_clone = cancellation_token.clone(); + let heartbeat_task_span = node_span.clone(); + task_handles.push(tokio::spawn( + async move { + let started_at = Instant::now(); + loop { + tokio::select! { + _ = tokio::time::sleep(tokio::time::Duration::from_secs(60)) => { + tracing::info!( + event = "service_heartbeat", + service = "bitvm-noded", + role = %heartbeat_actor, + peer_id = %heartbeat_peer_id, + uptime_secs = started_at.elapsed().as_secs(), + "node service heartbeat" + ); + } + _ = cancel_token_clone.cancelled() => { + return Ok("heartbeat stopped after cancellation".to_string()); + } + } } } - })); + .instrument(heartbeat_task_span), + )); + task_names.push("heartbeat"); // Wait for shutdown signal or any task completion let task_count = task_handles.len(); + tracing::info!( + event = "service_ready", + service = "bitvm-noded", + role = %actor, + peer_id = %peer_id_string, + task_count, + "all node background tasks have been started" + ); tokio::select! { (result, index, remaining_handles) = future::select_all(task_handles) => { + let task_name = task_names[index]; // Log the specific failure let failure_reason = match &result { Ok(Ok(tag)) => { - tracing::warn!("Task {} completed unexpectedly: {}", index, tag); + tracing::warn!( + event = "core_task_result", + service = "bitvm-noded", + task = task_name, + outcome = "unexpected_completion", + detail = %tag, + "node background task completed unexpectedly" + ); "unexpected completion" } Ok(Err(error)) => { - tracing::error!("Task {} failed with business error: {}", index, error); + tracing::error!( + event = "core_task_result", + service = "bitvm-noded", + task = task_name, + outcome = "business_error", + error = %error, + "node background task failed" + ); "business error" } Err(join_error) => { - tracing::error!("Task {} failed with join error: {}", index, join_error); + tracing::error!( + event = "core_task_result", + service = "bitvm-noded", + task = task_name, + outcome = "join_error", + error = %join_error, + "node background task failed to join" + ); "join error" } }; - tracing::info!("Triggering shutdown due to {} in task {}/{}", failure_reason, index + 1, task_count); + tracing::info!( + event = "service_shutdown", + service = "bitvm-noded", + trigger = "core_task_result", + task = task_name, + reason = failure_reason, + task_count, + "triggering node shutdown after background task result" + ); // Initiate graceful shutdown cancellation_token.cancel(); @@ -338,7 +505,12 @@ async fn main() -> Result<(), Box> { // Force abort any tasks that didn't respond to cancellation remaining_handles.into_iter().for_each(|handle| handle.abort()); - tracing::info!("All tasks stopped"); + tracing::info!( + event = "service_shutdown", + service = "bitvm-noded", + outcome = "tasks_stopped", + "all node background tasks stopped" + ); // Handle panic propagation if let Err(join_error) = result && join_error.is_panic() { @@ -347,12 +519,23 @@ async fn main() -> Result<(), Box> { } } _ = shutdown_signal() => { - tracing::info!("Received shutdown signal, initiating graceful shutdown..."); + tracing::info!( + event = "service_shutdown", + service = "bitvm-noded", + outcome = "started", + trigger = "signal", + "received shutdown signal; initiating graceful shutdown" + ); cancellation_token.cancel(); // Give tasks some time to shutdown gracefully tokio::time::sleep(tokio::time::Duration::from_secs(2)).await; - tracing::info!("Graceful shutdown completed"); + tracing::info!( + event = "service_shutdown", + service = "bitvm-noded", + outcome = "completed", + "node graceful shutdown completed" + ); } } @@ -378,10 +561,18 @@ async fn shutdown_signal() { tokio::select! { _ = ctrl_c => { - tracing::info!("Received Ctrl+C signal, starting graceful shutdown..."); + tracing::info!( + event = "service_shutdown_signal", + signal = "SIGINT", + "received Ctrl+C signal" + ); }, _ = terminate => { - tracing::info!("Received SIGTERM signal, starting graceful shutdown..."); + tracing::info!( + event = "service_shutdown_signal", + signal = "SIGTERM", + "received SIGTERM signal" + ); }, } } @@ -391,9 +582,6 @@ pub async fn start_handle_swarm_msg_task( mut swarm: BitvmNetworkManager, handler: BitvmNodeProcessor, cancellation_token: CancellationToken, -) -> String { - swarm.run(actor, handler, cancellation_token).await.unwrap_or_else(|e| { - tracing::error!("Swarm run error: {}", e); - "swarm_error".to_string() - }) +) -> anyhow::Result { + swarm.run(actor, handler, cancellation_token).await } diff --git a/node/src/metrics_service.rs b/node/src/metrics_service.rs index e0a2570d..3daf4ea4 100644 --- a/node/src/metrics_service.rs +++ b/node/src/metrics_service.rs @@ -120,6 +120,7 @@ pub async fn metrics_middleware( request.uri().path().to_owned() }; let method = request.method().to_string(); + state.metrics_state.http_requests_in_flight.inc(); let response = next.run(request).await; state.metrics_state.http_requests_in_flight.dec(); let status = response.status().as_u16(); diff --git a/node/src/rpc_service/bitvm.rs b/node/src/rpc_service/bitvm.rs index 15f0fc72..851b2d45 100644 --- a/node/src/rpc_service/bitvm.rs +++ b/node/src/rpc_service/bitvm.rs @@ -100,6 +100,12 @@ pub struct SendChallengeResponse { pub challenge_txid: String, } +#[derive(Debug, Deserialize, Serialize)] +pub struct SendVerifierChallengeResponse { + pub challenge_assert_txid: String, + pub verifier_index: usize, +} + #[derive(Debug, Deserialize, Serialize)] pub struct PegoutRequest { pub graph_id: Option, diff --git a/node/src/rpc_service/handler/bitvm2_handler.rs b/node/src/rpc_service/handler/bitvm2_handler.rs index ad4b9d90..834964d7 100644 --- a/node/src/rpc_service/handler/bitvm2_handler.rs +++ b/node/src/rpc_service/handler/bitvm2_handler.rs @@ -3,6 +3,7 @@ use crate::env::{ get_goat_address_from_env, get_goat_gateway_contract_from_env, get_network, get_node_goat_address, get_node_pubkey, }; +use crate::handle::broadcast_verifier_challenge_assert_tx; use crate::rpc_service::auth::verify_request_auth; use crate::rpc_service::bitvm::*; use crate::rpc_service::node::ALIVE_TIME_JUDGE_THRESHOLD; @@ -12,23 +13,23 @@ use crate::rpc_service::response::{ use crate::rpc_service::validation::InputValidator; use crate::rpc_service::{AppState, current_time_secs}; use crate::utils::{ - find_instances_by_escrow_hash, gen_instance_parameters_local, get_bridge_out_global_stats, - parse_graph_raw_data, send_challenge_tx, + bridge_out_instance_id_from_escrow_hash, find_instances_by_escrow_hash, + gen_instance_parameters_local, get_bridge_out_global_stats, load_validated_graph_definition, + send_challenge_tx, }; use alloy::primitives::{Address, U256}; use axum::Json; use axum::extract::{Path, Query, State}; use bitcoin::consensus::encode::serialize_hex; -use bitvm_lib::types::{BitvmGcGraph, SimplifiedBitvmGcGraph}; +use bitvm_lib::types::BitvmGcGraph; use client::goat_chain::{PeginStatus, WithdrawStatus}; use goat::transactions::pre_signed::PreSignedTransaction; use http::{HeaderMap, StatusCode}; -use sha2::{Digest, Sha256}; use std::default::Default; use std::str::FromStr; use std::sync::Arc; use std::time::{SystemTime, UNIX_EPOCH}; -use store::localdb::{GraphQuery, InstanceQuery, StorageProcessor}; +use store::localdb::{GraphQuery, InstanceQuery, InstanceUpdate, StorageProcessor}; use store::{ GoatTxType, Graph, GraphStatus, Instance, InstanceBridgeInStatus, InstanceBridgeOutStatus, }; @@ -36,19 +37,6 @@ use tokio::time::{Duration, sleep}; use tracing::warn; use uuid::Uuid; -const BRIDGE_OUT_INSTANCE_ID_PREFIX: [u8; 4] = *b"BOID"; - -fn bridge_out_instance_id_from_escrow_hash(escrow_hash: &str) -> Uuid { - let mut hasher = Sha256::new(); - hasher.update(b"bridge-out:"); - hasher.update(escrow_hash.as_bytes()); - let digest = hasher.finalize(); - let mut bytes = [0u8; 16]; - bytes.copy_from_slice(&digest[..16]); - bytes[..4].copy_from_slice(&BRIDGE_OUT_INSTANCE_ID_PREFIX); - Uuid::from_bytes(bytes) -} - fn bridge_out_retry_jitter_ms(attempt: u32) -> u64 { let now = SystemTime::now() .duration_since(UNIX_EPOCH) @@ -306,51 +294,31 @@ pub async fn bridge_out_init_tag( let current_time = current_time_secs(); for attempt in 0..=MAX_BRIDGE_OUT_INIT_RETRIES { - if let Some(mut instance) = - find_instances_by_escrow_hash(&mut storage_process, &escrow_hash) - .await - .api_error("PUT_BRIDGE_OUT_INIT_TAG_ERROR")? - { - if instance.status == InstanceBridgeOutStatus::Initialize.to_string() { - instance.to_addr = payload.to_addr.clone(); - instance.network = get_network().to_string(); - storage_process - .upsert_instance(&instance) - .await - .api_error("PUT_BRIDGE_OUT_INIT_TAG_ERROR")?; - } - return ok_response(BridgeOutInitTagResponse {}); - } - - let candidate_instance_id = if attempt == 0 { - bridge_out_instance_id_from_escrow_hash(&escrow_hash) - } else { - Uuid::new_v4() - }; - - if let Some(existing) = storage_process - .find_instance(&candidate_instance_id) + if let Some(instance) = find_instances_by_escrow_hash(&mut storage_process, &escrow_hash) .await .api_error("PUT_BRIDGE_OUT_INIT_TAG_ERROR")? { - let escrow_hash_matches = existing - .escrow_hash - .as_ref() - .map(|hash| hash.eq_ignore_ascii_case(&escrow_hash)) - .unwrap_or(false); - if !existing.is_bridge_in && escrow_hash_matches { - continue; - } - if attempt < MAX_BRIDGE_OUT_INIT_RETRIES { - sleep(Duration::from_millis(bridge_out_retry_jitter_ms(attempt))).await; - continue; + let updated = storage_process + .update_instance( + &InstanceUpdate::new_with_instance_id(instance.instance_id) + .with_to_addr(payload.to_addr.clone()) + .with_only_if_status_in(vec![ + InstanceBridgeOutStatus::Initialize.to_string(), + ]) + .with_only_if_is_bridge_in(false), + ) + .await + .api_error("PUT_BRIDGE_OUT_INIT_TAG_ERROR")?; + if !updated { + warn!( + "bridge_out_init_tag ignored for resolved instance {} with status {}", + instance.instance_id, instance.status + ); } - return error_response( - "BRIDGE_OUT_INSTANCE_ID_CONFLICT".to_string(), - "failed to allocate bridge-out instance_id for escrow_hash".to_string(), - ); + return ok_response(BridgeOutInitTagResponse {}); } + let candidate_instance_id = bridge_out_instance_id_from_escrow_hash(&escrow_hash); let mut instance = Instance { instance_id: candidate_instance_id, from_addr: from_addr.clone(), @@ -365,12 +333,58 @@ pub async fn bridge_out_init_tag( }; instance.to_addr = payload.to_addr.clone(); - match storage_process.upsert_instance(&instance).await { - Ok(_) => return ok_response(BridgeOutInitTagResponse {}), + match storage_process.insert_instance_if_absent(&instance).await { + Ok(true) => return ok_response(BridgeOutInitTagResponse {}), + Ok(false) => { + let Some(existing) = storage_process + .find_instance(&candidate_instance_id) + .await + .api_error("PUT_BRIDGE_OUT_INIT_TAG_ERROR")? + else { + if attempt < MAX_BRIDGE_OUT_INIT_RETRIES { + sleep(Duration::from_millis(bridge_out_retry_jitter_ms(attempt))).await; + continue; + } + return error_response( + "BRIDGE_OUT_INSTANCE_ID_CONFLICT".to_string(), + "bridge-out instance disappeared while being created".to_string(), + ); + }; + let escrow_hash_matches = existing + .escrow_hash + .as_ref() + .map(|hash| hash.eq_ignore_ascii_case(&escrow_hash)) + .unwrap_or(false); + if existing.is_bridge_in || !escrow_hash_matches { + return error_response( + "BRIDGE_OUT_INSTANCE_ID_CONFLICT".to_string(), + "failed to allocate bridge-out instance_id for escrow_hash".to_string(), + ); + } + + let updated = storage_process + .update_instance( + &InstanceUpdate::new_with_instance_id(existing.instance_id) + .with_to_addr(payload.to_addr.clone()) + .with_only_if_status_in(vec![ + InstanceBridgeOutStatus::Initialize.to_string(), + ]) + .with_only_if_is_bridge_in(false), + ) + .await + .api_error("PUT_BRIDGE_OUT_INIT_TAG_ERROR")?; + if !updated { + warn!( + "bridge_out_init_tag ignored for concurrently resolved instance {} with status {}", + existing.instance_id, existing.status + ); + } + return ok_response(BridgeOutInitTagResponse {}); + } Err(err) => { if attempt < MAX_BRIDGE_OUT_INIT_RETRIES { warn!( - "bridge_out_init_tag upsert failed at attempt {}, retrying: {}", + "bridge_out_init_tag insert failed at attempt {}, retrying: {}", attempt, err ); sleep(Duration::from_millis(bridge_out_retry_jitter_ms(attempt))).await; @@ -1202,22 +1216,27 @@ pub async fn get_graph_tx( let mut storage_process = app_state.local_db.acquire().await.api_error("GET_GRAPH_TX_ERROR")?; - let graph_raw_data = storage_process - .find_graph_raw_data(&graph_id_uuid) - .await - .api_error("GET_GRAPH_TX_ERROR")?; let graph = storage_process.find_graph(&graph_id_uuid).await.api_error("GET_GRAPH_TX_ERROR")?; - if let (Some(graph_raw_data), Some(graph)) = (graph_raw_data, graph) { + if let Some(graph) = graph { let (progresses, fail_reason) = get_graph_btc_tx_process_data(&mut storage_process, tx_name, &graph) .await .api_error("GET_GRAPH_TX_ERROR")?; - let simplified_bitvm_graph: SimplifiedBitvmGcGraph = - parse_graph_raw_data(graph_raw_data.raw_data.clone(), graph_id_uuid) + let simplified_bitvm_graph = + load_validated_graph_definition(&mut storage_process, graph.instance_id, graph_id_uuid) .await - .api_error("GET_GRAPH_TXN_ERROR")?; + .api_error("GET_GRAPH_TXN_ERROR")? + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: "GET_GRAPH_TXN_ERROR".to_string(), + message: format!("graph:{graph_id} raw data is not record in db"), + }), + ) + })?; let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(&simplified_bitvm_graph) .api_error("GET_GRAPH_TX_ERROR")?; @@ -1368,26 +1387,25 @@ pub async fn get_graph_txn( graph }; - let graph_raw_data = storage_processor - .find_graph_raw_data(&graph.graph_id) - .await - .api_error("GET_GRAPH_TXN_ERROR")? - .ok_or_else(|| { - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(ErrorResponse { - error: "GET_GRAPH_TXN_ERROR".to_string(), - message: format!( - "graph:{graph_id} with cursor:{} raw data is not record in db", - params.cursor - ), - }), - ) - })?; - let simplified_bitvm_graph: SimplifiedBitvmGcGraph = - parse_graph_raw_data(graph_raw_data.raw_data.clone(), graph.graph_id) - .await - .api_error("GET_GRAPH_TXN_ERROR")?; + let simplified_bitvm_graph = load_validated_graph_definition( + &mut storage_processor, + graph.instance_id, + graph.graph_id, + ) + .await + .api_error("GET_GRAPH_TXN_ERROR")? + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: "GET_GRAPH_TXN_ERROR".to_string(), + message: format!( + "graph:{graph_id} with cursor:{} raw data is not record in db", + params.cursor + ), + }), + ) + })?; let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(&simplified_bitvm_graph) .api_error("GET_GRAPH_TXN_ERROR")?; @@ -1616,8 +1634,8 @@ pub async fn send_challenge( let mut storage_process = app_state.local_db.acquire().await.api_error("SEND_CHALLENGE_ERROR")?; - let graph_raw_data = storage_process - .find_graph_raw_data(&graph_id_uuid) + let graph = storage_process + .find_graph(&graph_id_uuid) .await .api_error("SEND_CHALLENGE_ERROR")? .ok_or_else(|| { @@ -1625,15 +1643,24 @@ pub async fn send_challenge( StatusCode::INTERNAL_SERVER_ERROR, Json(ErrorResponse { error: "SEND_CHALLENGE_ERROR".to_string(), - message: format!("graph:{graph_id} raw data not found in db"), + message: format!("graph:{graph_id} not found in db"), }), ) })?; - let simplified_bitvm_graph: SimplifiedBitvmGcGraph = - parse_graph_raw_data(graph_raw_data.raw_data, graph_id_uuid) + let simplified_bitvm_graph = + load_validated_graph_definition(&mut storage_process, graph.instance_id, graph_id_uuid) .await - .api_error("SEND_CHALLENGE_ERROR")?; + .api_error("SEND_CHALLENGE_ERROR")? + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: "SEND_CHALLENGE_ERROR".to_string(), + message: format!("graph:{graph_id} raw data not found in db"), + }), + ) + })?; let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(&simplified_bitvm_graph).api_error("SEND_CHALLENGE_ERROR")?; @@ -1645,6 +1672,99 @@ pub async fn send_challenge( ok_response(SendChallengeResponse { challenge_txid: txid.to_string() }) } +/// Force the local verifier to broadcast its ChallengeAssert transaction for a graph. +/// +/// This is a test endpoint. Unlike the normal AssertSent verifier flow, it does +/// not skip ChallengeAssert when the operator assert proof is valid. +#[axum::debug_handler] +pub async fn send_verifier_challenge( + headers: HeaderMap, + Path(graph_id): Path, + State(app_state): State>, +) -> ApiResult { + verify_request_auth(&headers)?; + let graph_id_uuid = InputValidator::validate_uuid(&graph_id, "graph_id")?; + + let mut storage_process = + app_state.local_db.acquire().await.api_error("SEND_VERIFIER_CHALLENGE_ERROR")?; + + let graph = storage_process + .find_graph(&graph_id_uuid) + .await + .api_error("SEND_VERIFIER_CHALLENGE_ERROR")? + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: "SEND_VERIFIER_CHALLENGE_ERROR".to_string(), + message: format!("graph:{graph_id} not found in db"), + }), + ) + })?; + + let simplified_bitvm_graph = + load_validated_graph_definition(&mut storage_process, graph.instance_id, graph_id_uuid) + .await + .api_error("SEND_VERIFIER_CHALLENGE_ERROR")? + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: "SEND_VERIFIER_CHALLENGE_ERROR".to_string(), + message: format!("graph:{graph_id} raw data not found in db"), + }), + ) + })?; + + let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(&simplified_bitvm_graph) + .api_error("SEND_VERIFIER_CHALLENGE_ERROR")?; + + let assert_txid = bitvm_graph.operator_assert.tx().compute_txid(); + let assert_tx = app_state + .btc_client + .get_tx(&assert_txid) + .await + .api_error("SEND_VERIFIER_CHALLENGE_ERROR")? + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: "SEND_VERIFIER_CHALLENGE_ERROR".to_string(), + message: format!("operator assert tx {assert_txid} not found on Bitcoin"), + }), + ) + })?; + + let strict = true; + let (challenge_assert_txid, verifier_index) = broadcast_verifier_challenge_assert_tx( + &app_state.local_db, + &app_state.btc_client, + &bitvm_graph, + simplified_bitvm_graph.parameters.instance_parameters.instance_id, + graph_id_uuid, + &assert_tx, + strict, + ) + .await + .api_error("SEND_VERIFIER_CHALLENGE_ERROR")? + .ok_or_else(|| { + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(ErrorResponse { + error: "SEND_VERIFIER_CHALLENGE_ERROR".to_string(), + message: format!( + "local verifier cannot broadcast ChallengeAssert for graph {graph_id}" + ), + }), + ) + })?; + + ok_response(SendVerifierChallengeResponse { + challenge_assert_txid: challenge_assert_txid.to_string(), + verifier_index, + }) +} + const BTC_DECIMALS: u8 = 8; fn sats_to_token_amount(amount_sats: u64, token_decimals: u8) -> U256 { diff --git a/node/src/rpc_service/mod.rs b/node/src/rpc_service/mod.rs index 8e8d5208..8da2ccc0 100644 --- a/node/src/rpc_service/mod.rs +++ b/node/src/rpc_service/mod.rs @@ -16,11 +16,11 @@ use crate::rpc_service::handler::{ get_graph_neighbor_ids, get_graph_tx, get_graph_txn, get_graphs, get_instance, get_instance_escrow_data, get_instances, get_instances_overview, get_node, get_nodes, get_nodes_overview, get_operator_proof_desc, get_ready_to_kickoff_graph, - get_unsigned_pegin_txn, instance_settings, pegout, send_challenge, + get_unsigned_pegin_txn, instance_settings, pegout, send_challenge, send_verifier_challenge, }; +use anyhow::Context; use axum::body::Body; use axum::extract::Request; -use axum::middleware::Next; use axum::response::Response; use axum::routing::put; use axum::{ @@ -31,8 +31,6 @@ use bitvm_lib::actors::Actor; use client::btc_chain::BTCClient; use client::goat_chain::GOATClient; use client::http_client::async_client::HttpAsyncClient; -use http::{HeaderMap, StatusCode}; -use http_body_util::BodyExt; use prometheus_client::registry::Registry; use std::sync::{Arc, Mutex}; use std::time::{Duration, UNIX_EPOCH}; @@ -41,8 +39,7 @@ use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; use tower_http::classify::ServerErrorsFailureClass; use tower_http::cors::CorsLayer; -use tower_http::trace::{DefaultMakeSpan, TraceLayer}; -use tracing::Level; +use tower_http::trace::TraceLayer; #[inline(always)] pub fn current_time_secs() -> i64 { @@ -144,6 +141,7 @@ pub async fn serve_with_app_state( app_state: Arc, cancellation_token: CancellationToken, ) -> anyhow::Result { + let node_span = tracing::Span::current(); let server = Router::new() .route(routes::ROOT, get(root)) .route(routes::v1::NODES_BASE, get(get_nodes)) @@ -164,45 +162,66 @@ pub async fn serve_with_app_state( .route(routes::v1::GRAPHS_TX_BY_ID, get(get_graph_tx)) .route(routes::v1::GRAPHS_NEIGHBOR_IDS, get(get_graph_neighbor_ids)) .route(routes::v1::GRAPHS_SEND_CHALLENGE, post(send_challenge)) + .route(routes::v1::GRAPHS_SEND_VERIFIER_CHALLENGE, post(send_verifier_challenge)) .route(routes::v1::PEGOUT, post(pegout)) .route(routes::v1::PROOFS_CHAIN_PROOFS_DESC, get(get_chain_proof_desc)) .route(routes::v1::PROOFS_OPERATOR_PROOF_DESC, get(get_operator_proof_desc)) .route(routes::METRICS, get(metrics_handler)) - .layer(middleware::from_fn(print_req_and_resp_detail)) .layer(create_secure_cors_layer()) .layer( TraceLayer::new_for_http() - .make_span_with(DefaultMakeSpan::new().level(Level::INFO)) + .make_span_with(move |request: &Request| { + tracing::info_span!( + parent: &node_span, + "http_request", + method = %request.method(), + path = request.uri().path(), + version = ?request.version(), + ) + }) .on_request(|request: &Request, _span: &tracing::Span| { tracing::info!( - "API Request: {} {}, {:?}, Headers: {:?}, Content-Type: {:?}", - request.method(), - request.uri(), - request.version(), - request.headers(), - request.headers().get("content-type") + event = "http_request", + method = %request.method(), + path = request.uri().path(), + content_type = ?request.headers().get("content-type"), + "RPC request received" ); }) .on_response( |response: &Response, latency: Duration, _span: &tracing::Span| { tracing::info!( - "API Response: - Status: {} - Latency: {:?}", - response.status(), - latency + event = "http_response", + status = %response.status(), + elapsed_ms = latency.as_millis() as u64, + "RPC response sent" ); }, ) .on_failure( - |error: ServerErrorsFailureClass, _latency: Duration, _span: &tracing::Span| { - tracing::error!("API Error: {:?}", error); + |error: ServerErrorsFailureClass, latency: Duration, _span: &tracing::Span| { + tracing::error!( + event = "http_request_failure", + error_class = ?error, + elapsed_ms = latency.as_millis() as u64, + "RPC request failed" + ); }, ), ) .layer(middleware::from_fn_with_state(app_state.clone(), metrics_middleware)) .with_state(app_state); - let listener = TcpListener::bind(addr).await.unwrap(); - tracing::info!("RPC listening on {}", listener.local_addr().unwrap()); + let listener = TcpListener::bind(&addr) + .await + .with_context(|| format!("failed to bind RPC listener to {addr}"))?; + let listening_addr = + listener.local_addr().context("failed to determine RPC listener address")?; + tracing::info!( + event = "rpc_listening", + address = %listening_addr, + "RPC listener started" + ); tokio::select! { result = axum::serve(listener, server) => { @@ -233,39 +252,6 @@ pub async fn serve( serve_with_app_state(addr, app_state, cancellation_token).await } -/// This method introduces performance overhead and is temporarily used for debugging with the frontend. -/// It will be removed afterwards. -async fn print_req_and_resp_detail( - _headers: HeaderMap, - req: Request, - next: Next, -) -> Result { - // TODO remove after the service stabilizes. - let mut print_str = format!( - "API Request: method:{}, uri:{}, content_type:{:?}, body:", - req.method(), - req.uri(), - req.headers().get("content-type") - ); - let (parts, body) = req.into_parts(); - let bytes = body.collect().await.unwrap().to_bytes(); - if !bytes.is_empty() { - print_str = format!("{print_str} {}", String::from_utf8_lossy(&bytes)); - } - tracing::debug!("{}", print_str); - let req = Request::from_parts(parts, axum::body::Body::from(bytes)); - let resp = next.run(req).await; - - let mut print_str = format!("API Response: status:{}, body:", resp.status(),); - let (parts, body) = resp.into_parts(); - let bytes = body.collect().await.unwrap().to_bytes(); - if !bytes.is_empty() { - print_str = format!("{print_str} {}", String::from_utf8_lossy(&bytes)); - } - tracing::debug!("{}", print_str); - Ok(Response::from_parts(parts, axum::body::Body::from(bytes))) -} - #[cfg(test)] mod tests { use crate::env::{ @@ -291,8 +277,11 @@ mod tests { use std::str::FromStr; use std::sync::{Arc, Mutex}; use std::time::Duration; - use store::localdb::LocalDB; - use store::{Graph, GraphStatus, Instance, InstanceBridgeInStatus, Node, create_local_db}; + use store::localdb::{GraphRuntimeUpdate, LocalDB, StorageProcessor}; + use store::{ + Graph, GraphStatus, GraphStatusSource, Instance, InstanceBridgeInStatus, Node, + create_local_db, + }; use tokio::time::sleep; use tokio_util::sync::CancellationToken; use tracing::{error, info}; @@ -398,6 +387,54 @@ mod tests { Ok(()) } + async fn seed_graph_runtime( + tx: &mut StorageProcessor<'_>, + graph: &Graph, + ) -> anyhow::Result<()> { + let target_status = GraphStatus::from_str(&graph.status)?; + let sub_status = graph.sub_status.clone(); + let challenge_txid = graph.challenge_txid.clone(); + let init_withdraw_tx_hash = graph.init_withdraw_tx_hash.clone(); + let bridge_out_start_at = graph.bridge_out_start_at; + let proceed_withdraw_height = graph.proceed_withdraw_height; + + let mut definition = graph.clone(); + definition.status = GraphStatus::OperatorPresigned.to_string(); + definition.sub_status.clear(); + definition.challenge_txid = None; + definition.init_withdraw_tx_hash = None; + definition.bridge_out_start_at = 0; + definition.proceed_withdraw_height = 0; + tx.upsert_graph_definition(&definition).await?; + + if target_status != GraphStatus::OperatorPresigned { + tx.transition_graph_status( + graph.instance_id, + graph.graph_id, + target_status, + GraphStatusSource::ChainReconcile, + (!sub_status.is_empty()).then_some(sub_status), + ) + .await?; + } + + let mut runtime = GraphRuntimeUpdate::new(graph.instance_id, graph.graph_id); + if let Some(challenge_txid) = challenge_txid { + runtime = runtime.with_challenge_txid(challenge_txid); + } + if let Some(init_withdraw_tx_hash) = init_withdraw_tx_hash { + runtime = runtime.with_init_withdraw_tx_hash(init_withdraw_tx_hash); + } + if bridge_out_start_at != 0 { + runtime = runtime.with_bridge_out_start_at(bridge_out_start_at); + } + if proceed_withdraw_height != 0 { + runtime = runtime.with_proceed_withdraw_height(proceed_withdraw_height); + } + tx.update_graph_runtime(&runtime).await?; + Ok(()) + } + async fn init_instance_graph_data( local_db: &LocalDB, instances: &[Instance], @@ -408,7 +445,7 @@ mod tests { tx.upsert_instance(instance).await?; } for graph in graphs { - tx.upsert_graph(graph).await?; + seed_graph_runtime(&mut tx, graph).await?; } tx.commit().await?; Ok(()) @@ -594,6 +631,7 @@ mod tests { status: graph_status.clone(), sub_status: "".to_string(), operator_pubkey: "".to_string(), + definition_hash: format!("fixture-{graph_id}"), next_prekickoff: None, cur_prekickoff_txid: None, force_skip_kickoff_txid: None, @@ -618,8 +656,9 @@ mod tests { created_at: current_time_secs(), updated_at: current_time_secs(), }); + let finalized_graph_id = Uuid::new_v4(); graphs.push(Graph { - graph_id: Uuid::new_v4(), + graph_id: finalized_graph_id, instance_id: bridge_in_instance_id, kickoff_index: 0, from_addr: graph_from.clone(), @@ -629,6 +668,7 @@ mod tests { status: GraphStatus::CommitteePresigned.to_string(), sub_status: "".to_string(), operator_pubkey: "".to_string(), + definition_hash: format!("fixture-{finalized_graph_id}"), next_prekickoff: None, cur_prekickoff_txid: None, force_skip_kickoff_txid: None, diff --git a/node/src/rpc_service/routes.rs b/node/src/rpc_service/routes.rs index 1b023c26..5b4b082b 100644 --- a/node/src/rpc_service/routes.rs +++ b/node/src/rpc_service/routes.rs @@ -21,6 +21,7 @@ pub(crate) mod v1 { pub const GRAPHS_NEIGHBOR_IDS: &str = "/v1/graphs/{:id}/neighbor-ids"; pub const GRAPHS_TX_BY_ID: &str = "/v1/graphs/{:id}/tx"; pub const GRAPHS_SEND_CHALLENGE: &str = "/v1/graphs/{:id}/send-challenge"; + pub const GRAPHS_SEND_VERIFIER_CHALLENGE: &str = "/v1/graphs/{:id}/send-verifier-challenge"; pub const PEGOUT: &str = "/v1/graphs/pegout"; // pub const PROOFS_BASE: &str = "/v1/proofs"; pub const PROOFS_CHAIN_PROOFS_DESC: &str = "/v1/proofs/chain_proofs_desc"; diff --git a/node/src/scheduled_tasks/event_watch_task.rs b/node/src/scheduled_tasks/event_watch_task.rs index ca8c344e..3cab9ae6 100644 --- a/node/src/scheduled_tasks/event_watch_task.rs +++ b/node/src/scheduled_tasks/event_watch_task.rs @@ -11,8 +11,9 @@ use crate::scheduled_tasks::get_timestamp_from_contract_data; use crate::utils::evm_swap_utils::IEscrowManager::EscrowData; use crate::utils::evm_swap_utils::{extract_claim_data_from_tx, extract_escrow_data_from_tx}; use crate::utils::{ - GenerateInstanceParams, find_instances_by_escrow_hash, generate_instance, - get_bridge_out_global_stats, outpoint_available, reflect_goat_address, strip_hex_prefix_owned, + GenerateInstanceParams, bridge_out_instance_id_from_escrow_hash, find_instances_by_escrow_hash, + generate_instance, get_bridge_out_global_stats, outpoint_available, reflect_goat_address, + strip_hex_prefix_owned, }; use alloy::primitives::{Address as EvmAddress, U256}; use alloy::sol_types::{SolType, SolValue}; @@ -39,11 +40,11 @@ use std::ops::AddAssign; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; -use store::localdb::{GraphUpdate, InstanceUpdate, LocalDB, NodeQuery, StorageProcessor}; +use store::localdb::{GraphRuntimeUpdate, InstanceUpdate, LocalDB, NodeQuery, StorageProcessor}; use store::{ - GoatTxProcessingStatus, GoatTxRecord, GoatTxType, GraphStatus, Instance, - InstanceBridgeInStatus, InstanceBridgeOutStatus, MessageState, WatchContract, - WatchContractStatus, + GoatTxProcessingStatus, GoatTxRecord, GoatTxType, GraphStatus, GraphStatusSource, + GraphStatusTransitionOutcome, Instance, InstanceBridgeInStatus, InstanceBridgeOutStatus, + MessageState, WatchContract, WatchContractStatus, }; use tokio::time::sleep; use tokio_util::sync::CancellationToken; @@ -292,13 +293,20 @@ async fn handle_user_withdraw_events<'a>( UserGraphWithdrawEvent::InitWithdraw(init_event) => { let instance_id = Uuid::from_str(&strip_hex_prefix_owned(&init_event.instance_id))?; let graph_id = Uuid::from_str(&strip_hex_prefix_owned(&init_event.graph_id))?; - storage_processor - .update_graph( - &GraphUpdate::new(graph_id) + if !graph_belongs_to_instance(storage_processor, instance_id, graph_id).await? { + continue; + } + if !storage_processor + .update_graph_runtime( + &GraphRuntimeUpdate::new(instance_id, graph_id) .with_bridge_out_start_at(current_time_secs()) .with_init_withdraw_tx_hash(init_event.transaction_hash.clone()), ) - .await?; + .await? + { + warn!("ignore InitWithdraw for missing graph {instance_id}:{graph_id}"); + continue; + } storage_processor .upsert_goat_tx_record(&GoatTxRecord { instance_id, @@ -317,13 +325,20 @@ async fn handle_user_withdraw_events<'a>( let instance_id = Uuid::from_str(&strip_hex_prefix_owned(&cancel_event.instance_id))?; let graph_id = Uuid::from_str(&strip_hex_prefix_owned(&cancel_event.graph_id))?; - storage_processor - .update_graph( - &GraphUpdate::new(graph_id) + if !graph_belongs_to_instance(storage_processor, instance_id, graph_id).await? { + continue; + } + if !storage_processor + .update_graph_runtime( + &GraphRuntimeUpdate::new(instance_id, graph_id) .with_bridge_out_start_at(0) .with_init_withdraw_tx_hash("".to_string()), ) - .await?; + .await? + { + warn!("ignore CancelWithdraw for missing graph {instance_id}:{graph_id}"); + continue; + } storage_processor .upsert_goat_tx_record(&GoatTxRecord { instance_id, @@ -356,25 +371,33 @@ async fn handle_proceed_withdraw_events<'a>( for event in proceed_withdraw_events { let graph_id = Uuid::from_str(&strip_hex_prefix_owned(&event.graph_id))?; let instance_id = Uuid::from_str(&strip_hex_prefix_owned(&event.instance_id))?; + if !graph_belongs_to_instance(storage_processor, instance_id, graph_id).await? { + continue; + } + let height = event.block_number.parse::()?; + if !storage_processor + .update_graph_runtime( + &GraphRuntimeUpdate::new(instance_id, graph_id) + .with_proceed_withdraw_height(height), + ) + .await? + { + warn!("ignore ProceedWithdraw for missing graph {instance_id}:{graph_id}"); + continue; + } storage_processor .upsert_goat_tx_record(&GoatTxRecord { instance_id, graph_id, tx_type: GoatTxType::ProceedWithdraw.to_string(), tx_hash: event.transaction_hash, - height: event.block_number.parse::()?, + height, is_local: false, processing_status: processing_status.clone(), extra: None, created_at: current_time_secs(), }) .await?; - storage_processor - .update_graph( - &GraphUpdate::new(graph_id) - .with_proceed_withdraw_height(event.block_number.parse::()?), - ) - .await?; // for history events storage_processor @@ -389,43 +412,104 @@ async fn handle_proceed_withdraw_events<'a>( Ok(()) } +async fn apply_gateway_graph_status<'a>( + storage_processor: &mut StorageProcessor<'a>, + instance_id: Uuid, + graph_id: Uuid, + target: GraphStatus, +) -> anyhow::Result { + match storage_processor + .transition_graph_status(instance_id, graph_id, target, GraphStatusSource::GoatEvent, None) + .await? + { + outcome @ (GraphStatusTransitionOutcome::Applied + | GraphStatusTransitionOutcome::AlreadyCurrent) => Ok(outcome), + GraphStatusTransitionOutcome::Rejected { current } => { + warn!( + "ignore stale gateway graph event: graph={graph_id}, target={target}, current={current}" + ); + Ok(GraphStatusTransitionOutcome::Rejected { current }) + } + GraphStatusTransitionOutcome::NotFound => { + warn!("ignore gateway graph event for missing graph {graph_id}: target={target}"); + Ok(GraphStatusTransitionOutcome::NotFound) + } + } +} + +async fn graph_belongs_to_instance<'a>( + storage_processor: &mut StorageProcessor<'a>, + instance_id: Uuid, + graph_id: Uuid, +) -> anyhow::Result { + match storage_processor.find_graph(&graph_id).await? { + Some(graph) if graph.instance_id == instance_id => Ok(true), + Some(graph) => { + warn!( + "ignore gateway event with mismatched graph instance: graph={graph_id}, event_instance={instance_id}, stored_instance={}", + graph.instance_id + ); + Ok(false) + } + None => { + warn!("ignore gateway event for missing graph {instance_id}:{graph_id}"); + Ok(false) + } + } +} + async fn handle_withdraw_paths_events<'a>( storage_processor: &mut StorageProcessor<'a>, withdraw_paths_events: Vec, ) -> anyhow::Result<()> { for event in withdraw_paths_events { - let reward_add = U256::from_str(&event.reward_amount_str()).unwrap_or_default(); - let (flag, goat_addr) = reflect_goat_address(Some(event.operator_addr())); - if !flag { - warn!( - "handle_withdraw_paths_events failed as cast operator address failed, detail: {}, {}", - event.tx_hash(), - event.operator_addr() - ); - continue; - } - add_node_reward(storage_processor, &goat_addr.unwrap(), reward_add).await?; let (graph_id, instance_id, tx_type, status) = match event.clone() { WithdrawPathsEvent::WithdrawHappyEvent(v) => ( v.graph_id.clone(), v.instance_id.clone(), GoatTxType::WithdrawHappyPath.to_string(), - GraphStatus::OperatorTake1.to_string(), + GraphStatus::OperatorTake1, ), WithdrawPathsEvent::WithdrawUnhappyEvent(v) => ( v.graph_id.clone(), v.instance_id.clone(), GoatTxType::WithdrawUnhappyPath.to_string(), - GraphStatus::OperatorTake2.to_string(), + GraphStatus::OperatorTake2, ), }; let graph_id = Uuid::from_str(&strip_hex_prefix_owned(&graph_id))?; let instance_id = Uuid::from_str(&strip_hex_prefix_owned(&instance_id))?; + if !graph_belongs_to_instance(storage_processor, instance_id, graph_id).await? { + continue; + } + + let reward_add = U256::from_str(&event.reward_amount_str()).unwrap_or_default(); + let (flag, goat_addr) = reflect_goat_address(Some(event.operator_addr())); + if !flag { + warn!( + "handle_withdraw_paths_events failed as cast operator address failed, detail: {}, {}", + event.tx_hash(), + event.operator_addr() + ); + continue; + } + let outcome = + apply_gateway_graph_status(storage_processor, instance_id, graph_id, status).await?; + if !matches!( + outcome, + GraphStatusTransitionOutcome::Applied | GraphStatusTransitionOutcome::AlreadyCurrent + ) { + continue; + } + let is_new_event = storage_processor + .find_graph_goat_tx_record(&instance_id, &graph_id, &tx_type) + .await? + .is_none(); storage_processor .upsert_goat_tx_record(&GoatTxRecord { instance_id, graph_id, - tx_type, + tx_type: tx_type.clone(), tx_hash: event.tx_hash(), height: event.get_block_number(), is_local: false, @@ -434,8 +518,9 @@ async fn handle_withdraw_paths_events<'a>( created_at: current_time_secs(), }) .await?; - storage_processor.update_graph(&GraphUpdate::new(graph_id).with_status(status)).await?; - // cancel unfinished p2p message + if is_new_event { + add_node_reward(storage_processor, &goat_addr.unwrap(), reward_add).await?; + } storage_processor .update_messages_state_by_business_id( &graph_id, @@ -454,6 +539,10 @@ async fn handle_withdraw_disproved_events<'a>( ) -> anyhow::Result<()> { for event in withdraw_disproved_events { let graph_id = Uuid::from_str(&strip_hex_prefix_owned(&event.graph_id))?; + let instance_id = Uuid::from_str(&strip_hex_prefix_owned(&event.instance_id))?; + if !graph_belongs_to_instance(storage_processor, instance_id, graph_id).await? { + continue; + } let (flag, verifier_addr) = reflect_goat_address(Some(event.challenger_addr.clone())); if !flag { warn!( @@ -471,24 +560,51 @@ async fn handle_withdraw_disproved_events<'a>( continue; } - add_node_reward( + let outcome = apply_gateway_graph_status( storage_processor, - &verifier_addr.unwrap(), - U256::from_str(&event.challenger_amount_sats).unwrap_or_default(), - ) - .await?; - add_node_reward( - storage_processor, - &disprover_addr.unwrap(), - U256::from_str(&event.disprover_amount_sats).unwrap_or_default(), + instance_id, + graph_id, + GraphStatus::Disprove, ) .await?; + if !matches!( + outcome, + GraphStatusTransitionOutcome::Applied | GraphStatusTransitionOutcome::AlreadyCurrent + ) { + continue; + } + let tx_type = GoatTxType::WithdrawDisproved.to_string(); + let is_new_event = storage_processor + .find_graph_goat_tx_record(&instance_id, &graph_id, &tx_type) + .await? + .is_none(); storage_processor - .update_graph( - &GraphUpdate::new(graph_id).with_status(GraphStatus::Disprove.to_string()), + .upsert_goat_tx_record(&GoatTxRecord { + instance_id, + graph_id, + tx_type, + tx_hash: event.transaction_hash.clone(), + height: event.block_number.parse::()?, + is_local: false, + processing_status: GoatTxProcessingStatus::Pending.to_string(), + extra: None, + created_at: current_time_secs(), + }) + .await?; + if is_new_event { + add_node_reward( + storage_processor, + &verifier_addr.unwrap(), + U256::from_str(&event.challenger_amount_sats).unwrap_or_default(), + ) + .await?; + add_node_reward( + storage_processor, + &disprover_addr.unwrap(), + U256::from_str(&event.disprover_amount_sats).unwrap_or_default(), ) .await?; - // cancel unfinished p2p message + } storage_processor .update_messages_state_by_business_id( &graph_id, @@ -631,69 +747,114 @@ async fn handle_swap_init_events<'a>( .await? { let create_time = event.block_timestamp.parse::()?; - let (instance_id, instance) = if let Some(instance) = + let event_height = event.block_number.parse::()?; + let (instance_id, initialized) = if let Some(instance) = find_instances_by_escrow_hash(storage_processor, &event.escrow_hash).await? { - if instance.status != InstanceBridgeOutStatus::Initialize.to_string() { - (instance.instance_id, None) - } else { - (instance.instance_id, Some(instance)) - } + let initialized = storage_processor + .update_instance( + &swap_init_instance_update( + instance.instance_id, + &escrow_data, + &event, + event_height, + ) + .with_only_if_status_in(vec![ + InstanceBridgeOutStatus::Initialize.to_string(), + ]) + .with_only_if_is_bridge_in(false) + .with_only_if_goat_tx_hash(String::new()), + ) + .await?; + (instance.instance_id, initialized) } else { - let instance_id = Uuid::new_v4(); - ( + let instance_id = bridge_out_instance_id_from_escrow_hash(&event.escrow_hash); + let instance = Instance { instance_id, - Some(Instance { - instance_id, - is_bridge_in: false, - network: get_network().to_string(), - from_addr: escrow_data.offerer.to_string(), - input_utxos: "[]".to_string(), - status: InstanceBridgeOutStatus::Initialize.to_string(), - escrow_hash: Some(event.escrow_hash.clone()), - status_updated_at: create_time, - created_at: create_time, - ..Default::default() - }), - ) - }; - if let Some(mut instance) = instance { - instance.bridge_out_amount = escrow_data.amount.to_string(); - instance.goat_tx_hash = event.transaction_hash.clone(); - instance.goat_tx_height = event.block_number.parse::()?; - instance.user_change_addr = escrow_data.claimer.to_string(); - instance.user_refund_addr = escrow_data.claimer.to_string(); - instance.bridge_out_lock_time = - get_timestamp_from_contract_data(&escrow_data.refundData.0); - instance.status_updated_at = create_time; - storage_processor.upsert_instance(&instance).await?; - if escrow_data.token == *gateway_peg_btc_address { - let mut bridge_out_global_stats = - get_bridge_out_global_stats(storage_processor).await?; - let mut initial_amount = - U256::from_str(&bridge_out_global_stats.initial_amount).unwrap_or_default(); - initial_amount.add_assign(&escrow_data.amount); - bridge_out_global_stats.initial_amount = initial_amount.to_string(); - bridge_out_global_stats.initial_txn += 1; + is_bridge_in: false, + network: get_network().to_string(), + from_addr: escrow_data.offerer.to_string(), + input_utxos: "[]".to_string(), + status: InstanceBridgeOutStatus::Initialize.to_string(), + escrow_hash: Some(event.escrow_hash.clone()), + bridge_out_amount: escrow_data.amount.to_string(), + goat_tx_hash: event.transaction_hash.clone(), + goat_tx_height: event_height, + user_change_addr: escrow_data.claimer.to_string(), + user_refund_addr: escrow_data.claimer.to_string(), + bridge_out_lock_time: get_timestamp_from_contract_data( + &escrow_data.refundData.0, + ), + status_updated_at: create_time, + created_at: create_time, + ..Default::default() + }; + let initialized = if storage_processor.insert_instance_if_absent(&instance).await? { + true + } else if let Some(existing) = storage_processor.find_instance(&instance_id).await? + { + let escrow_hash_matches = existing + .escrow_hash + .as_ref() + .map(|hash| hash.eq_ignore_ascii_case(&event.escrow_hash)) + .unwrap_or(false); + if existing.is_bridge_in || !escrow_hash_matches { + anyhow::bail!( + "bridge-out instance ID collision for escrow hash {}", + event.escrow_hash + ); + } storage_processor - .upsert_bridge_out_global_stats(&bridge_out_global_stats) - .await?; - info!( - "swap initialize stats included: tx_hash={}, escrow_hash={}, token={}, amount={}", - event.transaction_hash, - event.escrow_hash, - escrow_data.token, - escrow_data.amount, - ); + .update_instance( + &swap_init_instance_update( + existing.instance_id, + &escrow_data, + &event, + event_height, + ) + .with_only_if_status_in(vec![ + InstanceBridgeOutStatus::Initialize.to_string(), + ]) + .with_only_if_is_bridge_in(false) + .with_only_if_goat_tx_hash(String::new()), + ) + .await? } else { - info!( - "swap initialize stats skipped(non-pegBTC): tx_hash={}, escrow_hash={}, token={}, expect_token={}", - event.transaction_hash, - event.escrow_hash, - escrow_data.token, - gateway_peg_btc_address, + anyhow::bail!( + "bridge-out instance {} disappeared during creation", + instance_id ); - } + }; + (instance_id, initialized) + }; + if initialized && escrow_data.token == *gateway_peg_btc_address { + let mut bridge_out_global_stats = + get_bridge_out_global_stats(storage_processor).await?; + let mut initial_amount = + U256::from_str(&bridge_out_global_stats.initial_amount).unwrap_or_default(); + initial_amount.add_assign(&escrow_data.amount); + bridge_out_global_stats.initial_amount = initial_amount.to_string(); + bridge_out_global_stats.initial_txn += 1; + storage_processor.upsert_bridge_out_global_stats(&bridge_out_global_stats).await?; + info!( + "swap initialize stats included: tx_hash={}, escrow_hash={}, token={}, amount={}", + event.transaction_hash, + event.escrow_hash, + escrow_data.token, + escrow_data.amount, + ); + } else if initialized { + info!( + "swap initialize stats skipped(non-pegBTC): tx_hash={}, escrow_hash={}, token={}, expect_token={}", + event.transaction_hash, + event.escrow_hash, + escrow_data.token, + gateway_peg_btc_address, + ); + } else { + info!( + "swap initialize ignored for resolved or previously initialized instance {instance_id}" + ); } storage_processor .upsert_goat_tx_record(&GoatTxRecord { @@ -701,7 +862,7 @@ async fn handle_swap_init_events<'a>( graph_id: Uuid::nil(), tx_type: GoatTxType::SwapInitialize.to_string(), tx_hash: event.transaction_hash.clone(), - height: event.block_number.parse::()?, + height: event_height, is_local: false, processing_status: GoatTxProcessingStatus::Skipped.to_string(), extra: Some(hex::encode(escrow_data.abi_encode())), @@ -715,6 +876,21 @@ async fn handle_swap_init_events<'a>( Ok(()) } +fn swap_init_instance_update( + instance_id: Uuid, + escrow_data: &EscrowData, + event: &SwapInitializeEvent, + event_height: i64, +) -> InstanceUpdate { + InstanceUpdate::new_with_instance_id(instance_id) + .with_bridge_out_amount(escrow_data.amount.to_string()) + .with_goat_tx_hash(event.transaction_hash.clone()) + .with_goat_tx_height(event_height) + .with_user_change_addr(escrow_data.claimer.to_string()) + .with_user_refund_addr(escrow_data.claimer.to_string()) + .with_bridge_out_lock_time(get_timestamp_from_contract_data(&escrow_data.refundData.0)) +} + async fn handle_swap_claim_events<'a>( storage_processor: &mut StorageProcessor<'a>, goat_client: Arc, @@ -740,6 +916,25 @@ async fn handle_swap_claim_events<'a>( bitcoin::Script::from_bytes(&claim_data.output_script), get_network(), )?; + let transitioned = storage_processor + .update_instance( + &InstanceUpdate::new_with_instance_id(instance_id) + .with_status(InstanceBridgeOutStatus::Claim.to_string()) + .with_btc_txid(claim_data.txid.into()) + .with_to_addr(to_addr.to_string()) + .with_only_if_status_in(vec![ + InstanceBridgeOutStatus::Initialize.to_string(), + ]) + .with_only_if_is_bridge_in(false), + ) + .await?; + if !transitioned { + info!( + "swap claim ignored for resolved instance {instance_id} with status {}", + instance.status + ); + continue; + } storage_processor .upsert_goat_tx_record(&GoatTxRecord { instance_id, @@ -753,14 +948,6 @@ async fn handle_swap_claim_events<'a>( created_at: current_time_secs(), }) .await?; - storage_processor - .update_instance( - &InstanceUpdate::new_with_escrow_hash(event.escrow_hash.clone()) - .with_status(InstanceBridgeOutStatus::Claim.to_string()) - .with_btc_txid(claim_data.txid.into()) - .with_to_addr(to_addr.to_string()), - ) - .await?; let is_peg_btc_swap = is_gateway_peg_btc_swap_instance( storage_processor, @@ -807,12 +994,23 @@ async fn handle_swap_refund_events<'a>( if let Some(instance) = find_instances_by_escrow_hash(storage_processor, &event.escrow_hash).await? { - storage_processor + let transitioned = storage_processor .update_instance( - &InstanceUpdate::new_with_escrow_hash(event.escrow_hash.clone()) - .with_status(InstanceBridgeOutStatus::Refund.to_string()), + &InstanceUpdate::new_with_instance_id(instance.instance_id) + .with_status(InstanceBridgeOutStatus::Refund.to_string()) + .with_only_if_status_in(vec![ + InstanceBridgeOutStatus::Initialize.to_string(), + ]) + .with_only_if_is_bridge_in(false), ) .await?; + if !transitioned { + info!( + "swap refund ignored for resolved instance {} with status {}", + instance.instance_id, instance.status + ); + continue; + } let is_peg_btc_swap = is_gateway_peg_btc_swap_instance( storage_processor, &instance.instance_id, @@ -932,15 +1130,20 @@ async fn handle_post_graph_data_events<'a>( post_graph_data_events: Vec, ) -> anyhow::Result<()> { for event in post_graph_data_events { - if let Ok(graph_id) = Uuid::from_str(&strip_hex_prefix_owned(&event.graph_id)) { - storage_processor - .update_graph( - &GraphUpdate::new(graph_id) - .with_status(GraphStatus::OperatorDataPushed.to_string()), + match ( + Uuid::from_str(&strip_hex_prefix_owned(&event.instance_id)), + Uuid::from_str(&strip_hex_prefix_owned(&event.graph_id)), + ) { + (Ok(instance_id), Ok(graph_id)) => { + let _ = apply_gateway_graph_status( + storage_processor, + instance_id, + graph_id, + GraphStatus::OperatorDataPushed, ) .await?; - } else { - warn!("failed to parse instance id:{event:?}"); + } + _ => warn!("failed to parse graph event identifiers: {event:?}"), } } Ok(()) diff --git a/node/src/scheduled_tasks/graph_maintenance_tasks.rs b/node/src/scheduled_tasks/graph_maintenance_tasks.rs index 6489e2d4..0d917e37 100644 --- a/node/src/scheduled_tasks/graph_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/graph_maintenance_tasks.rs @@ -7,13 +7,15 @@ use crate::action::{ use crate::env::get_network; use crate::rpc_service::current_time_secs; use crate::scheduled_tasks::fetch_on_turn_graph_by_status; -use crate::utils::{SELF_SENDER, outpoint_spent_txid, parse_graph_raw_data, upsert_message}; +use crate::utils::{ + SELF_SENDER, load_validated_graph_definition, outpoint_spent_txid, upsert_message, +}; use bitcoin::Txid; use bitvm_lib::actors::Actor; use bitvm_lib::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, validate_timelock_config, watchtower_challenge_timelock_blocks, + connector_f_timelock_blocks, disprove_timelock_blocks, operator_ack_timelock_blocks, + operator_commit_timelock_blocks, take1_timelock_blocks, take2_timelock_blocks, + validate_timelock_config, watchtower_challenge_timelock_blocks, }; use client::btc_chain::BTCClient; use client::goat_chain::DisproveTxType; @@ -77,16 +79,14 @@ async fn graph_timelock_config( local_db: &LocalDB, graph_id: Uuid, ) -> anyhow::Result { - let raw_data = { - let mut storage_processor = local_db.acquire().await?; - storage_processor.find_graph_raw_data(&graph_id).await? - }; - let Some(raw_data) = raw_data else { - warn!("graph {graph_id} raw data is missing, fallback to default timelock config"); - return Ok(default_timelock_config(get_network())); - }; - - let graph = parse_graph_raw_data(raw_data.raw_data, graph_id).await?; + let mut storage_processor = local_db.acquire().await?; + let graph_row = storage_processor.find_graph(&graph_id).await?.ok_or_else(|| { + anyhow::anyhow!("graph {graph_id} is missing while loading its timelock config") + })?; + let graph = + load_validated_graph_definition(&mut storage_processor, graph_row.instance_id, graph_id) + .await? + .ok_or_else(|| anyhow::anyhow!("graph {graph_id} has no validated raw definition"))?; validate_timelock_config( graph.parameters.instance_parameters.network, &graph.parameters.timelock_config, diff --git a/node/src/scheduled_tasks/instance_maintenance_tasks.rs b/node/src/scheduled_tasks/instance_maintenance_tasks.rs index 6d5d886c..cd4e1b15 100644 --- a/node/src/scheduled_tasks/instance_maintenance_tasks.rs +++ b/node/src/scheduled_tasks/instance_maintenance_tasks.rs @@ -112,14 +112,12 @@ async fn update_instance<'a>( storage_processor: &mut StorageProcessor<'a>, params: &InstanceUpdate, ) -> anyhow::Result<()> { - if let Err(err) = storage_processor.update_instance(params).await { - warn!( - "update_instance_status with input: {:?} failed {}, will try later", - params, - err.to_string() - ); - } else { - info!("update instance with input: {:?}", params); + match storage_processor.update_instance(params).await { + Ok(true) => info!("update instance with input: {:?}", params), + Ok(false) => info!("skip stale instance update with input: {:?}", params), + Err(err) => { + warn!("update_instance_status with input: {:?} failed {}, will try later", params, err); + } } Ok(()) } @@ -227,44 +225,77 @@ pub async fn instance_window_expiration_monitor( .await?; let committee_quorum_size = goat_client.committee_mana_quorum_size().await?; - for mut instance in instances { - match goat_client.gateway_get_pegin_data(&instance.instance_id).await { - Ok(pegin_data) => { - for (committee_addr, pubkey) in - pegin_data.committee_addresses.iter().zip(pegin_data.committee_pubkeys) - { - instance.committees_answers.insert(committee_addr.to_string(), pubkey); - } - - if committee_quorum_size <= instance.committees_answers.len() as u64 { - instance.status = InstanceBridgeInStatus::CommitteesAnswered.to_string(); - if let Err(err) = update_pegin_txids(&mut instance) { - warn!( - "instance_window_expiration_monitor fail to update_pegin_txids for instance {}, err: {:?}", - instance.instance_id, err - ); - } - } else { - instance.status = - InstanceBridgeInStatus::NoEnoughCommitteesAnswered.to_string(); - } - let mut storage_processor = local_db.acquire().await?; - if let Err(err) = storage_processor.upsert_instance(&instance).await { - warn!( - "failed to upsert instance {}, err: {}", - instance.instance_id, - err.to_string() - ); - } - } + for snapshot in instances { + let pegin_data = match goat_client.gateway_get_pegin_data(&snapshot.instance_id).await { + Ok(pegin_data) => pegin_data, Err(err) => { warn!( "failed to get pegin data for instance {}, err: {}", - instance.instance_id, - err.to_string() + snapshot.instance_id, err + ); + continue; + } + }; + + // The RPC call above can take arbitrarily long. 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. + let mut storage_processor = local_db.start_immediate_transaction().await?; + let Some(mut instance) = storage_processor.find_instance(&snapshot.instance_id).await? + else { + storage_processor.commit().await?; + continue; + }; + if instance.status != InstanceBridgeInStatus::UserInited.to_string() { + info!( + "skip expired response-window reconciliation for instance {} in status {}", + instance.instance_id, instance.status + ); + storage_processor.commit().await?; + continue; + } + + for (committee_addr, pubkey) in + pegin_data.committee_addresses.iter().zip(pegin_data.committee_pubkeys) + { + instance.committees_answers.insert(committee_addr.to_string(), pubkey); + } + + let reached_quorum = committee_quorum_size <= instance.committees_answers.len() as u64; + let next_status = if reached_quorum { + if let Err(err) = update_pegin_txids(&mut instance) { + warn!( + "instance_window_expiration_monitor failed to update pegin txids for instance {}, err: {err:?}", + instance.instance_id ); } + InstanceBridgeInStatus::CommitteesAnswered + } else { + InstanceBridgeInStatus::NoEnoughCommitteesAnswered + }; + + let mut update = InstanceUpdate::new_with_instance_id(instance.instance_id) + .with_status(next_status.to_string()) + .with_committees_answers(instance.committees_answers.clone()) + .with_only_if_status_in(vec![InstanceBridgeInStatus::UserInited.to_string()]) + .with_only_if_is_bridge_in(true); + if reached_quorum { + if let Some(btc_txid) = instance.btc_txid.clone() { + update = update.with_btc_txid(btc_txid); + } + if let Some(pegin_confirm_txid) = instance.pegin_confirm_txid.clone() { + update = update.with_pegin_confirm_txid(pegin_confirm_txid); + } + if let Some(pegin_cancel_txid) = instance.pegin_cancel_txid.clone() { + update = update.with_pegin_cancel_txid(pegin_cancel_txid); + } + } + + if !storage_processor.update_instance(&update).await? { + warn!("skip stale response-window update for instance {}", instance.instance_id); } + storage_processor.commit().await?; } Ok(()) @@ -494,7 +525,9 @@ pub async fn instance_bridge_out_monitor(local_db: &LocalDB) -> anyhow::Result<( .await?; let mut storage_processor = local_db.acquire().await?; for instance in instances { - let mut instance_update = InstanceUpdate::new_with_instance_id(instance.instance_id); + let mut instance_update = InstanceUpdate::new_with_instance_id(instance.instance_id) + .with_only_if_status_in(vec![InstanceBridgeOutStatus::Initialize.to_string()]) + .with_only_if_is_bridge_in(false); let lock_time = if instance.bridge_out_lock_time == 0 { let lock_time = get_bridge_out_deadline(&mut storage_processor, &instance.instance_id).await?; diff --git a/node/src/scheduled_tasks/mod.rs b/node/src/scheduled_tasks/mod.rs index c9432496..6a91e0a1 100644 --- a/node/src/scheduled_tasks/mod.rs +++ b/node/src/scheduled_tasks/mod.rs @@ -11,6 +11,7 @@ use crate::env::{ get_maintenance_run_timeout_secs, is_enable_babe_setup_state_cleanup, is_enable_update_spv_contract, is_relayer, }; +use crate::rpc_service::current_time_secs; use crate::scheduled_tasks::babe_setup_state_cleanup_task::babe_setup_state_cleanup_monitor; use crate::scheduled_tasks::graph_maintenance_tasks::{ detect_init_withdraw_call, detect_kickoff, detect_take1_or_challenge, process_graph_challenge, @@ -27,12 +28,13 @@ use client::btc_chain::BTCClient; use client::goat_chain::GOATClient; pub use event_watch_task::{is_processing_gateway_history_events, run_watch_event_task}; pub use sequencer_set_hash_monitor_task::run_sequencer_set_hash_monitor_task; +use std::future::Future; use std::sync::Arc; use std::time::{Duration, Instant}; use store::localdb::{LocalDB, StorageProcessor}; use store::{Graph, MessageType}; use tokio_util::sync::CancellationToken; -use tracing::{error, info, warn}; +use tracing::{debug, error, info, warn}; async fn fetch_on_turn_graph_by_status<'a>( storage_processor: &mut StorageProcessor<'a>, @@ -51,78 +53,124 @@ async fn fetch_on_turn_graph_by_status<'a>( } Ok(graphs) } + +async fn run_maintenance_subtask( + task: &'static str, + operation: impl Future>, +) { + let started_at = Instant::now(); + match operation.await { + Ok(_) => debug!( + event = "maintenance_subtask_result", + task, + outcome = "succeeded", + elapsed_ms = started_at.elapsed().as_millis() as u64, + "maintenance subtask completed" + ), + Err(error) => warn!( + event = "maintenance_subtask_result", + task, + outcome = "failed", + elapsed_ms = started_at.elapsed().as_millis() as u64, + error_class = "maintenance", + error = %error, + "maintenance subtask failed after execution" + ), + } +} + +enum MaintenanceRunOutcome { + Completed, + DeferredHistorySync, +} + async fn run( actor: Actor, local_db: &LocalDB, btc_client: Arc, goat_client: Arc, -) -> anyhow::Result<()> { +) -> anyhow::Result { let btc_client = btc_client.as_ref(); let goat_client = goat_client.as_ref(); if is_enable_babe_setup_state_cleanup() && matches!(&actor, Actor::Verifier | Actor::Operator | Actor::All) - && let Err(err) = babe_setup_state_cleanup_monitor(local_db).await { - warn!("babe_setup_state_cleanup_monitor, err {:?}", err) + run_maintenance_subtask( + "babe_setup_state_cleanup_monitor", + babe_setup_state_cleanup_monitor(local_db), + ) + .await; } - if (actor == Actor::Operator || is_relayer()) - && let Err(err) = node_available_pbtc_update_monitor(local_db, goat_client).await - { - warn!("node_available_pbtc_update_monitor, err {:?}", err) + if actor == Actor::Operator || is_relayer() { + run_maintenance_subtask( + "node_available_pbtc_update_monitor", + node_available_pbtc_update_monitor(local_db, goat_client), + ) + .await; } - if is_enable_update_spv_contract() - && let Err(err) = spv_header_hash_update(btc_client, goat_client).await - { - warn!("spv_header_hash_update, err {:?}", err) + if is_enable_update_spv_contract() { + run_maintenance_subtask( + "spv_header_hash_update", + spv_header_hash_update(btc_client, goat_client), + ) + .await; } if is_processing_gateway_history_events(local_db, goat_client).await? { - warn!("Still in history events processing"); - return Ok(()); - } - - if let Err(err) = instance_answers_monitor(local_db, btc_client, goat_client).await { - warn!("instance_answers_monitor, err {:?}", err) - } - if let Err(err) = instance_window_expiration_monitor(local_db, goat_client).await { - warn!("instance_window_expiration_monitor, err {:?}", err) - } - - if let Err(err) = instance_expiration_monitor(local_db, btc_client).await { - warn!("instance_expiration_monitor, err {:?}", err) - } - - if let Err(err) = instance_btc_tx_monitor(local_db, btc_client).await { - warn!("instance_btc_tx_monitor, err {:?}", err) - } - - if let Err(err) = instance_committee_key_cleanup_monitor(local_db, btc_client).await { - warn!("instance_committee_key_cleanup_monitor, err {:?}", err) - } - - if let Err(err) = instance_bridge_out_monitor(local_db).await { - warn!("instance_bridge_out_monitor, err {:?}", err) - } - - if let Err(err) = detect_init_withdraw_call(local_db).await { - warn!("detect_init_withdraw_call, err {:?}", err) - } - - if let Err(err) = detect_kickoff(local_db, btc_client).await { - warn!("detect_kickoff, err {:?}", err) + info!( + event = "maintenance_subtask_result", + task = "gateway_history_sync", + outcome = "deferred", + role = %actor, + reason = "history_sync_in_progress", + "maintenance protocol work deferred while gateway history sync is active" + ); + return Ok(MaintenanceRunOutcome::DeferredHistorySync); } - if let Err(err) = detect_take1_or_challenge(local_db, btc_client).await { - warn!("detect_take1_or_challenge, err {:?}", err) - } - - if let Err(err) = process_graph_challenge(local_db, btc_client).await { - warn!("process_grpah_challenge, err {:?}", err) - } - Ok(()) + run_maintenance_subtask( + "instance_answers_monitor", + instance_answers_monitor(local_db, btc_client, goat_client), + ) + .await; + run_maintenance_subtask( + "instance_window_expiration_monitor", + instance_window_expiration_monitor(local_db, goat_client), + ) + .await; + run_maintenance_subtask( + "instance_expiration_monitor", + instance_expiration_monitor(local_db, btc_client), + ) + .await; + run_maintenance_subtask( + "instance_btc_tx_monitor", + instance_btc_tx_monitor(local_db, btc_client), + ) + .await; + run_maintenance_subtask( + "instance_committee_key_cleanup_monitor", + instance_committee_key_cleanup_monitor(local_db, btc_client), + ) + .await; + run_maintenance_subtask("instance_bridge_out_monitor", instance_bridge_out_monitor(local_db)) + .await; + run_maintenance_subtask("detect_init_withdraw_call", detect_init_withdraw_call(local_db)).await; + run_maintenance_subtask("detect_kickoff", detect_kickoff(local_db, btc_client)).await; + run_maintenance_subtask( + "detect_take1_or_challenge", + detect_take1_or_challenge(local_db, btc_client), + ) + .await; + run_maintenance_subtask( + "process_graph_challenge", + process_graph_challenge(local_db, btc_client), + ) + .await; + Ok(MaintenanceRunOutcome::Completed) } pub async fn run_maintenance_tasks( @@ -140,26 +188,102 @@ pub async fn run_maintenance_tasks( _ = tokio::time::sleep(Duration::from_secs(interval)) => { tick += 1; let tick_start = Instant::now(); - info!(tick, interval_secs = interval, "maintenance task tick start"); + info!( + event = "maintenance_tick", + tick, + interval_secs = interval, + outcome = "started", + "maintenance task tick started" + ); // Execute the normal monitoring logic match tokio::time::timeout( maintenance_run_timeout, run(actor.clone(),&local_db,btc_client.clone(),goat_client.clone()), ).await { - Ok(Ok(_)) => {} - Ok(Err(err)) => {error!("run_scheduled_tasks, err {:?}", err)} + Ok(Ok(MaintenanceRunOutcome::Completed)) => { + info!( + event = "maintenance_tick_result", + tick, + outcome = "succeeded", + elapsed_ms = tick_start.elapsed().as_millis() as u64, + "maintenance task tick completed" + ); + } + Ok(Ok(MaintenanceRunOutcome::DeferredHistorySync)) => { + info!( + event = "maintenance_tick_result", + tick, + outcome = "deferred", + reason = "history_sync_in_progress", + elapsed_ms = tick_start.elapsed().as_millis() as u64, + "maintenance protocol work was deferred" + ); + } + Ok(Err(error)) => { + error!( + event = "maintenance_tick_result", + tick, + outcome = "failed", + elapsed_ms = tick_start.elapsed().as_millis() as u64, + error = %error, + "maintenance task returned an error" + ) + } Err(_) => { error!( + event = "maintenance_tick_result", tick, + outcome = "timed_out", timeout_secs = maintenance_run_timeout.as_secs(), - "maintenance run timeout" + elapsed_ms = tick_start.elapsed().as_millis() as u64, + "maintenance task tick timed out" ) } } - info!(tick, elapsed_ms = tick_start.elapsed().as_millis() as u64, "maintenance task tick end"); + if tick.is_multiple_of(6) { + let queue_started_at = Instant::now(); + match local_db.acquire().await { + Ok(mut storage) => match storage + .get_message_queue_stats(&actor.to_string(), current_time_secs()) + .await + { + Ok(stats) => info!( + event = "message_queue_snapshot", + role = %actor, + pending_ready = stats.pending_ready, + pending_locked = stats.pending_locked, + failed = stats.failed, + oldest_pending_at = ?stats.oldest_pending_at, + elapsed_ms = queue_started_at.elapsed().as_millis() as u64, + "local message queue snapshot" + ), + Err(error) => error!( + event = "db_operation_result", + operation = "get_message_queue_stats", + outcome = "failed", + elapsed_ms = queue_started_at.elapsed().as_millis() as u64, + error = %error, + "failed to collect local message queue snapshot" + ), + }, + Err(error) => error!( + event = "db_operation_result", + operation = "acquire_db_for_message_queue_snapshot", + outcome = "failed", + elapsed_ms = queue_started_at.elapsed().as_millis() as u64, + error = %error, + "failed to acquire local database for message queue snapshot" + ), + } + } } _ = cancellation_token.cancelled() => { - tracing::info!("maintenance task received shutdown signal"); + tracing::info!( + event = "maintenance_lifecycle", + outcome = "shutdown", + role = %actor, + "maintenance task received shutdown signal" + ); return Ok("maintenance_shutdown".to_string()); } } diff --git a/node/src/utils.rs b/node/src/utils.rs index da6f792f..abb5a8de 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -1,7 +1,6 @@ use crate::action::{ ChallengeSent, DisproveSent, GOATMessage, GOATMessageContent, KickoffSent, NodeInfo, - PreKickoffSent, SolderingProofReady, Take1Sent, Take2Sent, push_local_unhandled_messages, - send_to_peer, + PreKickoffSent, SolderingProofReady, Take1Sent, Take2Sent, send_to_peer, }; use crate::env::*; use crate::error::SpecialError; @@ -68,12 +67,8 @@ use std::net::SocketAddr; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::{Path, PathBuf}; use std::str::FromStr; - -pub const SELF_SENDER: &str = "self"; -use std::time::{SystemTime, UNIX_EPOCH}; -use store::localdb::{ - GraphQuery, GraphUpdate, InstanceQuery, InstanceUpdate, LocalDB, StorageProcessor, -}; +use std::time::{Instant, SystemTime, UNIX_EPOCH}; +use store::localdb::{GraphQuery, GraphRuntimeUpdate, InstanceQuery, LocalDB, StorageProcessor}; use crate::env; use crate::rpc_service::routes::v1::{ @@ -98,9 +93,10 @@ use proof_builder::{ WatchtowerProofTimeoutUpdateResponse, }; use store::{ - BridgeOutGlobalStats, ByteArray32, Graph, GraphRawData, GraphStatus, Instance, - InstanceBridgeInStatus, Message, MessageState, MessageType, Node, PeginGraphProcessData, - PeginInstanceProcessData, SerializableTxid, UInt64Array3, + BridgeOutGlobalStats, ByteArray32, Graph, GraphRawData, GraphStatus, GraphStatusSource, + GraphStatusTransitionOutcome, Instance, InstanceBridgeInStatus, Message, MessageState, + MessageType, Node, PeginGraphProcessData, PeginInstanceProcessData, SerializableTxid, + UInt64Array3, }; use stun_client::{Attribute, Class, Client}; use tracing::{error, info, warn}; @@ -113,6 +109,29 @@ use zkm_verifier::{ load_ark_public_inputs_from_bytes, }; +pub const SELF_SENDER: &str = "self"; +const BRIDGE_OUT_INSTANCE_ID_PREFIX: [u8; 4] = *b"BOID"; + +/// Derive the shared bridge-out instance ID from its escrow hash. +/// +/// Both the RPC tag endpoint and the chain-event watcher must use this ID so +/// their concurrent create attempts collide on the primary key instead of +/// creating two rows for one escrow. +pub(crate) fn bridge_out_instance_id_from_escrow_hash(escrow_hash: &str) -> Uuid { + let normalized_escrow_hash = escrow_hash + .strip_prefix("0x") + .or_else(|| escrow_hash.strip_prefix("0X")) + .unwrap_or(escrow_hash); + let mut hasher = Sha256::new(); + hasher.update(b"bridge-out:"); + hasher.update(normalized_escrow_hash.to_ascii_lowercase().as_bytes()); + let digest = hasher.finalize(); + let mut bytes = [0u8; 16]; + bytes.copy_from_slice(&digest[..16]); + bytes[..4].copy_from_slice(&BRIDGE_OUT_INSTANCE_ID_PREFIX); + Uuid::from_bytes(bytes) +} + pub(crate) const BRIDGE_OUT_GLOBAL_STATS_ID: i64 = 1; pub type VerifyingKey = ark_groth16::VerifyingKey; @@ -357,16 +376,17 @@ pub mod todo_funcs { ) .await?; if let Some(previous_graph) = graphs.first() { - let Some(graph_raw_data) = - storage_processor.find_graph_raw_data(&previous_graph.graph_id).await? - else { - bail!(SpecialError::InvalidGraph(format!( + let simplified_graph = super::load_validated_graph_definition( + &mut storage_processor, + previous_graph.instance_id, + previous_graph.graph_id, + ) + .await? + .ok_or_else(|| { + SpecialError::InvalidGraph(format!( "previous graph raw data is missing for graph nonce {previous_nonce}" - ))); - }; - let simplified_graph = - super::parse_graph_raw_data(graph_raw_data.raw_data, previous_graph.graph_id) - .await?; + )) + })?; let expected_cur_prekickoff = BitvmGcGraph::from_simplified(&simplified_graph)?.next_prekickoff; let expected_txid = expected_cur_prekickoff.finalize().compute_txid(); @@ -1126,29 +1146,20 @@ pub(crate) async fn refresh_graph( goat_client: &GOATClient, instance_id: Uuid, graph_id: Uuid, - graph: Option<&BitvmGcGraph>, - scan_from_status: Option, - scan_from_sub_status: Option, -) -> Result<(GraphStatus, Option, Option)> { - let Some(graph) = graph else { - let status = scan_from_status.unwrap_or(GraphStatus::OperatorPresigned); - return Ok((status, scan_from_sub_status, None)); - }; - - let scan = scan_graph_chain_state( - btc_client, - goat_client, - graph, - scan_from_status, - scan_from_sub_status, - ) - .await?; - - if let Some(challenge_txid) = scan.challenge_txid { - update_graph_challenge_txid_if_needed(local_db, graph_id, challenge_txid).await?; + graph: &BitvmGcGraph, +) -> Result { + if graph.parameters.instance_parameters.instance_id != instance_id + || graph.parameters.graph_id != graph_id + { + bail!( + "refuse to refresh graph {instance_id}:{graph_id} with mismatched graph parameters {}:{}", + graph.parameters.instance_parameters.instance_id, + graph.parameters.graph_id + ); } - update_graph_status( + let scan = scan_graph_chain_state(btc_client, goat_client, graph).await?; + let outcome = update_graph_status( local_db, instance_id, graph_id, @@ -1157,7 +1168,54 @@ pub(crate) async fn refresh_graph( ) .await?; - Ok((scan.status, Some(scan.sub_status.clone()), Some(scan))) + match outcome { + GraphStatusTransitionOutcome::Applied => { + if let Some(challenge_txid) = scan.challenge_txid { + update_graph_challenge_txid_if_needed(local_db, graph_id, challenge_txid).await?; + } + Ok(GraphRefreshResult { + status: scan.status, + sub_status: Some(scan.sub_status.clone()), + scan: Some(scan), + status_transition_accepted: true, + }) + } + GraphStatusTransitionOutcome::AlreadyCurrent => { + if let Some(challenge_txid) = scan.challenge_txid { + update_graph_challenge_txid_if_needed(local_db, graph_id, challenge_txid).await?; + } + Ok(GraphRefreshResult { + status: scan.status, + sub_status: Some(scan.sub_status.clone()), + // Preserve the verified scan even for an idempotent status + // replay. The previous attempt may have committed the status + // before it could enqueue the corresponding local message. + scan: Some(scan), + status_transition_accepted: true, + }) + } + GraphStatusTransitionOutcome::Rejected { current } => { + warn!( + "ignore stale chain scan for graph {instance_id}:{graph_id}: candidate={}, current={current}", + scan.status + ); + Ok(GraphRefreshResult { + status: current, + sub_status: None, + scan: None, + status_transition_accepted: false, + }) + } + GraphStatusTransitionOutcome::NotFound => { + warn!("graph {instance_id}:{graph_id} disappeared while applying chain scan"); + Ok(GraphRefreshResult { + status: scan.status, + sub_status: None, + scan: None, + status_transition_accepted: false, + }) + } + } } #[derive(Clone, Debug)] @@ -1180,11 +1238,15 @@ pub(crate) struct GraphChainScan { disprove: Option, } -fn normalize_challenge_sub_status( - mut sub_status: ChallengeSubStatus, - watchtower_num: usize, - verifier_num: usize, -) -> ChallengeSubStatus { +pub(crate) struct GraphRefreshResult { + pub(crate) status: GraphStatus, + pub(crate) sub_status: Option, + pub(crate) scan: Option, + pub(crate) status_transition_accepted: bool, +} + +fn initial_challenge_sub_status(watchtower_num: usize, verifier_num: usize) -> ChallengeSubStatus { + let mut sub_status = ChallengeSubStatus::default(); sub_status.watchtower_challenge_status.resize(watchtower_num, false); sub_status.verifier_challenge_status.resize(verifier_num, VerifierChallengeStatus::None); sub_status @@ -1227,7 +1289,10 @@ async fn update_graph_challenge_txid_if_needed( }; if graph.challenge_txid.as_ref().map(|txid| txid.0) != Some(challenge_txid) { storage_processor - .update_graph(&GraphUpdate::new(graph_id).with_challenge_txid(challenge_txid.into())) + .update_graph_runtime( + &GraphRuntimeUpdate::new(graph.instance_id, graph_id) + .with_challenge_txid(challenge_txid.into()), + ) .await?; } Ok(()) @@ -1329,34 +1394,19 @@ async fn scan_graph_chain_state( btc_client: &BTCClient, goat_client: &GOATClient, graph: &BitvmGcGraph, - scan_from_status: Option, - scan_from_sub_status: Option, ) -> Result { let instance_id = graph.parameters.instance_parameters.instance_id; let graph_id = graph.parameters.graph_id; let watchtower_num = graph.parameters.watchtower_pubkeys.len(); let verifier_num = graph.verifier_asserts.len(); - let mut sub_status = normalize_challenge_sub_status( - scan_from_sub_status.unwrap_or_default(), - watchtower_num, - verifier_num, - ); - let mut current_status = match scan_from_status { - Some(s) => s, - None => { - if graph.committee_pre_signed() { - GraphStatus::CommitteePresigned - } else { - return Ok(GraphChainScan { - status: GraphStatus::OperatorPresigned, - sub_status, - challenge_txid: None, - watchtower_challenge_init_on_chain: false, - operator_assert_on_chain: false, - disprove: None, - }); - } - } + let mut sub_status = initial_challenge_sub_status(watchtower_num, verifier_num); + // Derive the candidate from the graph and both chains, rather than using + // the locally persisted status as the scan start. The stored status is a + // projection and can be stale after a restart or missed listener event. + let mut current_status = if graph.committee_pre_signed() { + GraphStatus::CommitteePresigned + } else { + GraphStatus::OperatorPresigned }; let prekickoff_txid = graph.cur_prekickoff.tx().compute_txid(); @@ -1364,12 +1414,12 @@ async fn scan_graph_chain_state( let take1_txid = graph.take1.tx().compute_txid(); let take2_txid = graph.take2.tx().compute_txid(); - // check if Graph has been posted on GoatChain - if current_status == GraphStatus::CommitteePresigned { - let graph_data_on_goat = goat_client.gateway_get_graph_data(&graph_id).await?; - if graph_data_on_goat.operator_pubkey != [0u8; 32] { - current_status = GraphStatus::OperatorDataPushed; - } + // GraphData is a Goat-chain fact and must be checked regardless of the + // current local projection. A fully synced node may otherwise remain at + // OperatorPresigned forever after a prior status regression. + let graph_data_on_goat = goat_client.gateway_get_graph_data(&graph_id).await?; + if graph_data_on_goat.operator_pubkey != [0u8; 32] { + current_status = GraphStatus::OperatorDataPushed; } // check if Graph has been obsoleted on GoatChain if current_status == GraphStatus::OperatorDataPushed { @@ -1681,7 +1731,7 @@ async fn scan_graph_chain_state( } #[allow(clippy::enum_variant_names)] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] enum GraphCompensateEventKind { PreKickoffSent, // OperatorDataPushed -> PreKickoff KickoffSent, // PreKickoff -> OperatorKickOff @@ -1699,12 +1749,59 @@ fn map_transition_to_event(from: GraphStatus, to: GraphStatus) -> Option Some(GraphCompensateEventKind::Take1Sent), (OperatorKickOff, Challenge) => Some(GraphCompensateEventKind::ChallengeSent), (Challenge, Disprove) => Some(GraphCompensateEventKind::DisproveSent), - (OperatorKickOff, Disprove) => Some(GraphCompensateEventKind::DisproveSent), (Challenge, OperatorTake2) => Some(GraphCompensateEventKind::Take2Sent), _ => None, } } +/// The canonical protocol ancestry used only for recovering missed local +/// messages. It is deliberately separate from transition authorization: a +/// chain scan can authorize additional recovery edges such as +/// `Obsoleted -> OperatorDataPushed`, but those edges do not imply a P2P +/// phase message should be synthesized. +fn compensation_previous_status(status: GraphStatus) -> Option { + use GraphStatus::*; + + match status { + CommitteePresigned => Some(OperatorPresigned), + OperatorDataPushed => Some(CommitteePresigned), + PreKickoff | Obsoleted | Skipped => Some(OperatorDataPushed), + OperatorKickOff => Some(PreKickoff), + OperatorTake1 | Challenge => Some(OperatorKickOff), + Disprove | OperatorTake2 => Some(Challenge), + OperatorPresigned | Created | Presigned | L2Recorded | OperatorKickOffing | Challenging + | Disproving => None, + } +} + +fn compensation_events_from( + compensation_anchor_status: GraphStatus, + final_status: GraphStatus, +) -> Option> { + if compensation_anchor_status == final_status { + return Some(vec![]); + } + + let mut reverse_path = vec![final_status]; + let mut cursor = final_status; + while cursor != compensation_anchor_status { + let previous = compensation_previous_status(cursor)?; + reverse_path.push(previous); + cursor = previous; + } + reverse_path.reverse(); + + Some( + reverse_path + .windows(2) + .filter_map(|window| match window { + [from, to] => map_transition_to_event(*from, *to), + _ => None, + }) + .collect(), + ) +} + async fn upsert_graph_compensate_message( local_db: &LocalDB, graph_id: Uuid, @@ -1712,7 +1809,14 @@ async fn upsert_graph_compensate_message( actor: Actor, message_content: GOATMessageContent, ) -> Result<()> { - let mut storage_processor = local_db.acquire().await?; + let message_type = get_goat_message_content_type(&message_content); + let message_id = generate_message_id(graph_id, message_type.to_string(), sub_type.clone()); + let mut storage_processor = local_db.start_transaction().await?; + if !storage_processor.insert_graph_compensation_marker(graph_id, &message_id).await? { + storage_processor.commit().await?; + return Ok(()); + } + upsert_message( &mut storage_processor, false, @@ -1724,7 +1828,8 @@ async fn upsert_graph_compensate_message( 0, 0, ) - .await + .await?; + storage_processor.commit().await } async fn push_graph_compensate_message( @@ -1733,8 +1838,10 @@ async fn push_graph_compensate_message( actor: Actor, message_content: GOATMessageContent, ) -> Result<()> { - let message = GOATMessage::new(actor, message_content); - push_local_unhandled_messages(local_db, graph_id, &message, 0).await + // Unlike an action retry, an inferred chain event must not reset an + // existing queued message. This makes compensation safe to retry when the + // status write committed before the message was persisted. + upsert_graph_compensate_message(local_db, graph_id, None, actor, message_content).await } #[allow(dead_code)] @@ -1756,10 +1863,9 @@ pub(crate) async fn compensate_graph_events( _btc_client: &BTCClient, instance_id: Uuid, graph_id: Uuid, - _graph: Option<&BitvmGcGraph>, + _graph: &BitvmGcGraph, scan: Option<&GraphChainScan>, - scan_from_status: Option, - compensate_from_status: GraphStatus, + compensation_anchor_status: GraphStatus, final_status: GraphStatus, ) -> Result<()> { let Some(scan) = scan else { @@ -1769,41 +1875,17 @@ pub(crate) async fn compensate_graph_events( return Ok(()); }; - let scan_start = scan_from_status.unwrap_or(compensate_from_status); - let effective_from = if scan_start.is_after(&compensate_from_status) { - scan_start - } else { - compensate_from_status - }; - if !effective_from.is_before(&final_status) { + // The action anchor is the only input used to build a recovery path. The + // persisted status is a projection and must not manufacture or suppress + // messages after a restart. Every enqueue below is idempotent. + let Some(events) = compensation_events_from(compensation_anchor_status, final_status) else { tracing::debug!( - "Skip graph compensation for {instance_id}:{graph_id}: effective_from={effective_from:?}, final_status={final_status:?}" + "Skip graph compensation without a canonical observed path for {instance_id}:{graph_id}: anchor={compensation_anchor_status:?}, final={final_status:?}" ); return Ok(()); - } - - let mut rev_path = vec![final_status]; - let mut cursor = final_status; - while cursor != effective_from { - let Some(prev) = cursor.get_previous_status() else { - tracing::debug!( - "Skip graph compensation for {instance_id}:{graph_id}: cannot walk from {final_status:?} back to {effective_from:?}" - ); - return Ok(()); - }; - rev_path.push(prev); - cursor = prev; - } - rev_path.reverse(); - - for window in rev_path.windows(2) { - let [from, to] = window else { - continue; - }; - let Some(event) = map_transition_to_event(*from, *to) else { - continue; - }; + }; + for event in events { match event { GraphCompensateEventKind::PreKickoffSent => { push_graph_compensate_message( @@ -2229,8 +2311,35 @@ pub async fn broadcast_nonstandard_tx(btc_client: &BTCClient, tx: &Transaction) /// - The mempool API URL must be configured. /// - The transaction should already be fully signed. pub async fn broadcast_tx(client: &BTCClient, tx: &Transaction) -> Result<()> { - client.broadcast(tx).await?; - Ok(()) + let txid = tx.compute_txid(); + let started_at = Instant::now(); + match client.broadcast(tx).await { + Ok(()) => { + tracing::info!( + event = "btc_tx_broadcast", + outcome = "broadcasted", + txid = %txid, + input_count = tx.input.len(), + output_count = tx.output.len(), + elapsed_ms = started_at.elapsed().as_millis() as u64, + "bitcoin transaction broadcast accepted by client" + ); + Ok(()) + } + Err(err) => { + tracing::warn!( + event = "btc_tx_broadcast", + outcome = "failed", + txid = %txid, + input_count = tx.input.len(), + output_count = tx.output.len(), + elapsed_ms = started_at.elapsed().as_millis() as u64, + error = %err, + "bitcoin transaction broadcast failed" + ); + Err(err) + } + } } pub async fn broadcast_package( @@ -2238,13 +2347,29 @@ pub async fn broadcast_package( txns: &[Transaction], fallback_on_failure: bool, ) -> Result<()> { + let txids: Vec = txns.iter().map(Transaction::compute_txid).collect(); + let started_at = Instant::now(); match client.broadcast_package(txns).await { - Ok(_) => {} + Ok(_) => { + tracing::info!( + event = "btc_tx_package_broadcast", + outcome = "broadcasted", + transaction_count = txids.len(), + txids = ?txids, + elapsed_ms = started_at.elapsed().as_millis() as u64, + "bitcoin transaction package broadcast accepted by client" + ); + } Err(e) => { if fallback_on_failure { tracing::warn!( - "broadcast_package failed: {}, falling back to broadcasting one by one", - e + event = "btc_tx_package_broadcast", + outcome = "fallback_to_individual_broadcast", + transaction_count = txids.len(), + txids = ?txids, + elapsed_ms = started_at.elapsed().as_millis() as u64, + error = %e, + "bitcoin transaction package broadcast failed; falling back to individual broadcasts" ); for tx in txns { broadcast_tx(client, tx).await?; @@ -3734,6 +3859,7 @@ pub async fn upsert_message( lock_time_until: current_time_secs() + lock_time, state: MessageState::Pending.to_string(), message_version: 0, + created_at: 0, }) .await?; } else { @@ -4185,15 +4311,16 @@ pub async fn get_current_prekickoff_tx( ) .await?; - if !graphs.is_empty() - && let Some(graph_raw_data) = - storage_processor.find_graph_raw_data(&graphs[0].graph_id).await? + if let Some(graph) = graphs.first() + && let Some(simplified_graph) = load_validated_graph_definition( + &mut storage_processor, + graph.instance_id, + graph.graph_id, + ) + .await? { - let simplified_graph = - parse_graph_raw_data(graph_raw_data.raw_data, graphs[0].graph_id).await?; - Ok(Some(( - (graphs[0].kickoff_index + 1) as u64, + (graph.kickoff_index + 1) as u64, BitvmGcGraph::from_simplified(&simplified_graph)?.next_prekickoff, ))) } else { @@ -4329,12 +4456,12 @@ pub async fn get_instance_parameters( } } -fn convert_graph(bitvm_graph: &BitvmGcGraph, current_time: i64) -> Graph { - let mut status = GraphStatus::OperatorPresigned.to_string(); - if bitvm_graph.committee_pre_signed() { - status = GraphStatus::CommitteePresigned.to_string(); - } - +fn convert_graph( + bitvm_graph: &BitvmGcGraph, + current_time: i64, + initial_status: GraphStatus, + definition_hash: String, +) -> Graph { Graph { graph_id: bitvm_graph.parameters.graph_id, instance_id: bitvm_graph.parameters.instance_parameters.instance_id, @@ -4343,9 +4470,10 @@ fn convert_graph(bitvm_graph: &BitvmGcGraph, current_time: i64) -> Graph { to_addr: "".to_string(), amount: bitvm_graph.parameters.instance_parameters.pegin_amount.to_sat() as i64, challenge_amount: bitvm_graph.parameters.challenge_amount.to_sat() as i64, - status, + status: initial_status.to_string(), sub_status: "".to_string(), operator_pubkey: bitvm_graph.parameters.operator_pubkey.to_string(), + definition_hash, cur_prekickoff_txid: Some(bitvm_graph.cur_prekickoff.finalize().compute_txid().into()), next_prekickoff: Some(bitvm_graph.next_prekickoff.finalize().compute_txid().into()), force_skip_kickoff_txid: Some( @@ -4396,37 +4524,202 @@ fn convert_graph(bitvm_graph: &BitvmGcGraph, current_time: i64) -> Graph { } } -pub async fn store_graph(local_db: &LocalDB, simple_graph: &SimplifiedBitvmGcGraph) -> Result<()> { +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) enum FinalizedGraphStoreOutcome { + NewlyStored, + DefinitionUpgraded, + AlreadyFinalized, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum GraphDefinitionIngestKind { + OperatorPresigned, + Finalized, +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +enum GraphDefinitionIngestOutcome { + Inserted, + FinalizedUpgrade, + Replay, + AlreadyFinalized, +} + +/// Store a verified operator-pre-signed graph definition. +/// +/// This path is intentionally unable to advance `GraphStatus`: receiving a +/// CreateGraph message is not evidence of committee finalization. +pub(crate) async fn store_operator_presigned_graph( + local_db: &LocalDB, + simple_graph: &SimplifiedBitvmGcGraph, +) -> Result<()> { + if !simple_graph.operator_pre_signed() || simple_graph.committee_pre_signed() { + bail!(SpecialError::InvalidGraph(format!( + "graph {} is not an operator-pre-signed proposal", + simple_graph.parameters.graph_id + ))); + } + + let mut tx = local_db.start_transaction().await?; + ingest_graph_definition(&mut tx, simple_graph, GraphDefinitionIngestKind::OperatorPresigned) + .await?; + tx.commit().await?; + Ok(()) +} + +/// Store a verified finalized graph once without replacing a graph whose +/// finalized form is already known. +pub(crate) async fn store_finalized_graph_if_needed( + local_db: &LocalDB, + simple_graph: &SimplifiedBitvmGcGraph, +) -> Result { + let graph_id = simple_graph.parameters.graph_id; + if !simple_graph.operator_pre_signed() || !simple_graph.committee_pre_signed() { + bail!(SpecialError::InvalidGraph(format!("graph {graph_id} is not fully pre-signed"))); + } + let mut tx = local_db.start_transaction().await?; + let outcome = + ingest_graph_definition(&mut tx, simple_graph, GraphDefinitionIngestKind::Finalized) + .await?; + tx.commit().await?; + + Ok(match outcome { + GraphDefinitionIngestOutcome::Inserted => FinalizedGraphStoreOutcome::NewlyStored, + GraphDefinitionIngestOutcome::FinalizedUpgrade => { + FinalizedGraphStoreOutcome::DefinitionUpgraded + } + GraphDefinitionIngestOutcome::AlreadyFinalized => { + FinalizedGraphStoreOutcome::AlreadyFinalized + } + GraphDefinitionIngestOutcome::Replay => { + unreachable!("a finalized graph replay must be classified as already finalized") + } + }) +} + +async fn ingest_graph_definition( + tx: &mut StorageProcessor<'_>, + simple_graph: &SimplifiedBitvmGcGraph, + kind: GraphDefinitionIngestKind, +) -> Result { let bitvm_graph: BitvmGcGraph = BitvmGcGraph::from_simplified(simple_graph)?; let graph_id = simple_graph.parameters.graph_id; - let instance_id = simple_graph.parameters.instance_parameters.instance_id; + verify_graph_operator_pre_signatures(&bitvm_graph).map_err(|error| { + SpecialError::InvalidGraph(format!( + "graph {graph_id} has invalid operator pre-signatures: {error}" + )) + })?; + if kind == GraphDefinitionIngestKind::Finalized { + verify_graph_committee_pre_signatures(&bitvm_graph).map_err(|error| { + SpecialError::InvalidGraph(format!( + "graph {graph_id} has invalid committee pre-signatures: {error}" + )) + })?; + } let current_time = current_time_secs(); - let mut graph = convert_graph(&bitvm_graph, current_time); - let incoming_parameters_hash = simple_graph.parameters_hash()?; - - if let Some(existing_raw_data) = tx.find_graph_raw_data(&graph_id).await? { - let existing_graph = parse_graph_raw_data(existing_raw_data.raw_data, graph_id).await?; - let existing_parameters_hash = existing_graph.parameters_hash()?; - if existing_parameters_hash != incoming_parameters_hash { + let definition_hash = hex::encode(simple_graph.parameters_hash()?); + let existing_graph_row = tx.find_graph(&graph_id).await?; + let existing_raw_data = tx.find_graph_raw_data(&graph_id).await?; + let existing_graph = match (existing_graph_row.as_ref(), existing_raw_data) { + (None, None) => None, + (Some(_), None) => { bail!(SpecialError::InvalidGraph(format!( - "graph parameters changed for graph_id {graph_id}: existing={}, incoming={}", - hex::encode(existing_parameters_hash), - hex::encode(incoming_parameters_hash) + "graph {graph_id} has runtime data but no raw definition; explicit repair is required" ))); } - if existing_graph.operator_pre_signed() && !simple_graph.operator_pre_signed() { + (None, Some(_)) => { bail!(SpecialError::InvalidGraph(format!( - "graph {graph_id} cannot be downgraded after operator pre-signatures are stored" + "graph {graph_id} has raw definition but no graph row; explicit repair is required" ))); } - if existing_graph.committee_pre_signed() && !simple_graph.committee_pre_signed() { + (Some(existing_row), Some(existing_raw_data)) => { + if existing_row.definition_hash.is_empty() { + bail!(SpecialError::InvalidGraph(format!( + "graph {graph_id} is missing its stored definition hash; explicit repair is required" + ))); + } + if existing_row.definition_hash != definition_hash { + bail!(SpecialError::InvalidGraph(format!( + "graph parameters changed for graph_id {graph_id}: existing={}, incoming={definition_hash}", + existing_row.definition_hash + ))); + } + + let existing_graph = parse_graph_raw_data(existing_raw_data.raw_data, graph_id).await?; + let existing_raw_hash = hex::encode(existing_graph.parameters_hash()?); + if existing_raw_hash != existing_row.definition_hash { + bail!(SpecialError::InvalidGraph(format!( + "graph {graph_id} definition hash does not match its stored raw definition" + ))); + } + let existing_full_graph = BitvmGcGraph::from_simplified(&existing_graph)?; + verify_graph_operator_pre_signatures(&existing_full_graph).map_err(|error| { + SpecialError::InvalidGraph(format!( + "stored graph {graph_id} has invalid operator pre-signatures: {error}" + )) + })?; + if existing_full_graph.committee_pre_signed() { + verify_graph_committee_pre_signatures(&existing_full_graph).map_err(|error| { + SpecialError::InvalidGraph(format!( + "stored graph {graph_id} has invalid committee pre-signatures: {error}" + )) + })?; + } + Some(existing_graph) + } + }; + + let existing_is_finalized = + existing_graph.as_ref().is_some_and(SimplifiedBitvmGcGraph::committee_pre_signed); + if existing_is_finalized { + let existing_graph = existing_graph.as_ref().expect("checked above"); + if kind == GraphDefinitionIngestKind::Finalized && existing_graph != simple_graph { bail!(SpecialError::InvalidGraph(format!( - "graph {graph_id} cannot be downgraded after committee pre-signatures are stored" + "conflicting finalized graph for graph_id {graph_id}" ))); } + + // An old CreateGraph replay is harmless once a verified finalized + // graph is stored. Do not replace its raw signatures or runtime. + if kind == GraphDefinitionIngestKind::Finalized { + confirm_finalized_graph_definition( + tx, + bitvm_graph.parameters.instance_parameters.instance_id, + graph_id, + ) + .await?; + } + return Ok(GraphDefinitionIngestOutcome::AlreadyFinalized); } + if kind == GraphDefinitionIngestKind::OperatorPresigned + && let Some(existing_graph) = existing_graph.as_ref() + && existing_graph != simple_graph + { + bail!(SpecialError::InvalidGraph(format!( + "conflicting operator-pre-signed graph for graph_id {graph_id}" + ))); + } + if kind == GraphDefinitionIngestKind::Finalized + && let Some(existing_graph) = existing_graph.as_ref() + && existing_graph.operator_pre_sigs != simple_graph.operator_pre_sigs + { + bail!(SpecialError::InvalidGraph(format!( + "finalized graph {graph_id} changes the stored operator pre-signatures" + ))); + } + + let mut graph = convert_graph( + &bitvm_graph, + current_time, + // A graph row is always created at the operator-presigned baseline. + // Only a verified finalized definition may advance it through the + // dedicated Definition transition below. + GraphStatus::OperatorPresigned, + definition_hash.clone(), + ); + if let Some(node_info) = tx.get_node_by_btc_pub_key(&bitvm_graph.parameters.operator_pubkey.to_string()).await? { @@ -4435,26 +4728,80 @@ pub async fn store_graph(local_db: &LocalDB, simple_graph: &SimplifiedBitvmGcGra node_p2wsh_address(get_network(), &bitvm_graph.parameters.operator_pubkey).to_string(); } - tx.upsert_graph(&graph).await?; - if bitvm_graph.committee_pre_signed() { - tx.update_instance( - &InstanceUpdate::new_with_instance_id(instance_id) - .with_status(InstanceBridgeInStatus::Presigned.to_string()), + tx.upsert_graph_definition(&graph).await?; + + let outcome = if existing_graph.is_none() { + let raw_data = serialize_graph_raw_data(simple_graph, graph_id).await?; + tx.upsert_graph_raw_data( + bitvm_graph.parameters.instance_parameters.instance_id, + GraphRawData { graph_id, raw_data, created_at: current_time, updated_at: current_time }, + &definition_hash, + ) + .await?; + if kind == GraphDefinitionIngestKind::Finalized { + confirm_finalized_graph_definition( + tx, + bitvm_graph.parameters.instance_parameters.instance_id, + graph_id, + ) + .await?; + } + GraphDefinitionIngestOutcome::Inserted + } else if kind == GraphDefinitionIngestKind::Finalized { + let raw_data = serialize_graph_raw_data(simple_graph, graph_id).await?; + tx.upsert_graph_raw_data( + bitvm_graph.parameters.instance_parameters.instance_id, + GraphRawData { graph_id, raw_data, created_at: current_time, updated_at: current_time }, + &definition_hash, ) .await?; - } - let raw_data = serialize_graph_raw_data(simple_graph, graph_id).await?; - tx.upsert_graph_raw_data(GraphRawData { - graph_id, - raw_data, - created_at: current_time, - updated_at: current_time, - }) - .await?; + // Only a verified finalized definition can advance an existing locally + // operator-pre-signed graph. A later chain-observed state is retained. + confirm_finalized_graph_definition( + tx, + bitvm_graph.parameters.instance_parameters.instance_id, + graph_id, + ) + .await?; + GraphDefinitionIngestOutcome::FinalizedUpgrade + } else { + GraphDefinitionIngestOutcome::Replay + }; - tx.commit().await?; - Ok(()) + Ok(outcome) +} + +/// Confirm that a fully verified graph definition has reached the committee +/// pre-signing stage without allowing it to overwrite a later runtime state. +async fn confirm_finalized_graph_definition( + tx: &mut StorageProcessor<'_>, + instance_id: Uuid, + graph_id: Uuid, +) -> Result<()> { + match tx + .transition_graph_status( + instance_id, + graph_id, + GraphStatus::CommitteePresigned, + GraphStatusSource::Definition, + None, + ) + .await? + { + GraphStatusTransitionOutcome::Applied | GraphStatusTransitionOutcome::AlreadyCurrent => { + Ok(()) + } + GraphStatusTransitionOutcome::Rejected { current } => { + tracing::debug!( + "keep later graph status while storing finalized definition: graph={graph_id}, current={current}" + ); + Ok(()) + } + GraphStatusTransitionOutcome::NotFound => { + bail!("graph {graph_id} disappeared while confirming its finalized definition") + } + } } /// Parse raw graph data JSON string to SimplifiedBitvmGcGraph using spawn_blocking @@ -4530,19 +4877,80 @@ pub async fn serialize_graph_raw_data( } } -pub async fn get_graph( - local_db: &LocalDB, - _instance_id: Uuid, +/// Load a graph only after binding its runtime row, raw definition, canonical +/// parameters and pre-signatures together. Callers must use this instead of +/// parsing `graph_raw_data` directly. +pub(crate) async fn load_validated_graph_definition( + storage_processor: &mut StorageProcessor<'_>, + instance_id: Uuid, graph_id: Uuid, ) -> Result> { - let mut storage_process = local_db.acquire().await?; - if let Some(graph_raw_data) = storage_process.find_graph_raw_data(&graph_id).await? - && let Ok(simplified_graph) = parse_graph_raw_data(graph_raw_data.raw_data, graph_id).await + let Some(graph_row) = storage_processor.find_graph(&graph_id).await? else { + return Ok(None); + }; + if graph_row.instance_id != instance_id { + bail!(SpecialError::InvalidGraph(format!( + "graph {graph_id} belongs to instance {}, not {instance_id}", + graph_row.instance_id + ))); + } + if graph_row.definition_hash.is_empty() { + bail!(SpecialError::InvalidGraph(format!( + "graph {graph_id} is missing its definition hash; explicit repair is required" + ))); + } + + let graph_raw_data = storage_processor + .find_graph_raw_data(&graph_id) + .await? + .ok_or_else(|| { + SpecialError::InvalidGraph(format!( + "graph {graph_id} has a runtime row but no raw definition; explicit repair is required" + )) + })?; + let simplified_graph = parse_graph_raw_data(graph_raw_data.raw_data, graph_id).await?; + if simplified_graph.parameters.graph_id != graph_id + || simplified_graph.parameters.instance_parameters.instance_id != instance_id { - Ok(Some(simplified_graph)) - } else { - Ok(None) + bail!(SpecialError::InvalidGraph(format!( + "stored raw definition identifiers do not match graph {instance_id}:{graph_id}" + ))); + } + let definition_hash = hex::encode(simplified_graph.parameters_hash()?); + if definition_hash != graph_row.definition_hash { + bail!(SpecialError::InvalidGraph(format!( + "stored raw definition hash does not match graph row for {instance_id}:{graph_id}" + ))); } + + let full_graph = BitvmGcGraph::from_simplified(&simplified_graph).map_err(|error| { + SpecialError::InvalidGraph(format!( + "stored raw definition cannot rebuild graph for {instance_id}:{graph_id}: {error}" + )) + })?; + verify_graph_operator_pre_signatures(&full_graph).map_err(|error| { + SpecialError::InvalidGraph(format!( + "stored raw definition has invalid operator pre-signatures for {instance_id}:{graph_id}: {error}" + )) + })?; + if full_graph.committee_pre_signed() { + verify_graph_committee_pre_signatures(&full_graph).map_err(|error| { + SpecialError::InvalidGraph(format!( + "stored raw definition has invalid committee pre-signatures for {instance_id}:{graph_id}: {error}" + )) + })?; + } + + Ok(Some(simplified_graph)) +} + +pub async fn get_graph( + local_db: &LocalDB, + instance_id: Uuid, + graph_id: Uuid, +) -> Result> { + let mut storage_processor = local_db.acquire().await?; + load_validated_graph_definition(&mut storage_processor, instance_id, graph_id).await } pub async fn get_graph_by_instance_id_and_operator_pubkey( @@ -4554,10 +4962,8 @@ pub async fn get_graph_by_instance_id_and_operator_pubkey( if let Some(graph_id) = storage_process .get_graph_id_by_instance_id_and_operator_pubkey(&instance_id, &operator_pubkey.to_string()) .await? - && let Some(graph_raw_data) = storage_process.find_graph_raw_data(&graph_id).await? - && let Ok(simplified_graph) = parse_graph_raw_data(graph_raw_data.raw_data, graph_id).await { - Ok(Some(simplified_graph)) + load_validated_graph_definition(&mut storage_process, instance_id, graph_id).await } else { Ok(None) } @@ -4964,24 +5370,58 @@ pub async fn get_verifier_graph_params_endorsements_for_graph( .filter_map(|(k, v)| { v.verifier_index .zip(v.verifier_params_signature.as_ref()) - .map(|(index, signature)| (*k, index, signature.clone())) + .map(|(index, signature)| (*k, index, *signature)) }) .collect()) } pub async fn mark_graph_as_endorsed( local_db: &LocalDB, - _instance_id: Uuid, + instance_id: Uuid, graph_id: Uuid, ) -> Result<()> { let mut storage_processor = local_db.acquire().await?; - storage_processor.update_pegin_graph_endorsed(&graph_id, true).await?; - Ok(()) + let (_, process_data) = find_pegin_graph_process_data(&mut storage_processor, graph_id).await?; + upsert_pegin_graph_process_data( + &mut storage_processor, + graph_id, + instance_id, + true, + &process_data, + ) + .await } pub async fn get_endorsed_graph_count(local_db: &LocalDB, instance_id: Uuid) -> Result { let mut storage_processor = local_db.acquire().await?; Ok(storage_processor.get_pegin_graph_endorsed_len_by_instance_id(&instance_id, true).await? as usize) } + +pub async fn has_required_presigned_graphs(local_db: &LocalDB, instance_id: Uuid) -> Result { + Ok(get_endorsed_graph_count(local_db, instance_id).await? + >= todo_funcs::min_required_operator()) +} + +pub async fn try_transition_instance_to_presigned( + local_db: &LocalDB, + instance_id: Uuid, +) -> Result { + if !has_required_presigned_graphs(local_db, instance_id).await? { + return Ok(false); + } + + let mut storage_processor = local_db.acquire().await?; + let transitioned = storage_processor + .update_instance_status_if_current( + &instance_id, + &InstanceBridgeInStatus::UserBroadcastPeginPrepare.to_string(), + &InstanceBridgeInStatus::Presigned.to_string(), + ) + .await?; + if transitioned { + info!("Instance {instance_id} reached the required finalized graph threshold"); + } + Ok(transitioned) +} pub async fn store_committee_pub_nonce_for_instance( local_db: &LocalDB, instance_id: Uuid, @@ -5174,7 +5614,10 @@ pub async fn try_update_graph_challenge_txid( ); let mut storage_processor = local_db.acquire().await?; storage_processor - .update_graph(&GraphUpdate::new(graph_id).with_challenge_txid(spent_txid.into())) + .update_graph_runtime( + &GraphRuntimeUpdate::new(graph.instance_id, graph_id) + .with_challenge_txid(spent_txid.into()), + ) .await?; } else { info!("try_update_graph_challenge_txid no need to challenge_txid for graph {graph_id}"); @@ -5188,42 +5631,17 @@ pub async fn update_graph_status( graph_id: Uuid, new_status: GraphStatus, sub_status: Option, -) -> Result<()> { +) -> Result { let mut storage_processor = local_db.acquire().await?; - match storage_processor.find_graph(&graph_id).await? { - Some(graph) => { - if graph.status == new_status.to_string() - && let Some(ref sub_status) = sub_status - && *sub_status == ChallengeSubStatus::default() - { - warn!( - "graph: {graph_id}, new_status: {new_status} is equal old status and ChallengeSubStatus is None, so not update" - ); - return Ok(()); - } - } - None => { - warn!("graph: {graph_id} is not update, so not update"); - return Ok(()); - } - } - - if new_status == GraphStatus::CommitteePresigned { - storage_processor - .update_instance( - &InstanceUpdate::new_with_instance_id(instance_id) - .with_status(InstanceBridgeInStatus::Presigned.to_string()), - ) - .await?; - } - - let mut graph_update = GraphUpdate::new(graph_id).with_status(new_status.to_string()); - if let Some(sub_status) = sub_status { - graph_update = graph_update.with_sub_status(serde_json::to_string(&sub_status)?); - } - - storage_processor.update_graph(&graph_update).await?; - Ok(()) + storage_processor + .transition_graph_status( + instance_id, + graph_id, + new_status, + GraphStatusSource::ChainReconcile, + sub_status.map(|sub_status| serde_json::to_string(&sub_status)).transpose()?, + ) + .await } pub async fn get_graph_ids_for_instance( local_db: &LocalDB, diff --git a/node/tla/GraphLifecycle.cfg b/node/tla/GraphLifecycle.cfg new file mode 100644 index 00000000..ff744585 --- /dev/null +++ b/node/tla/GraphLifecycle.cfg @@ -0,0 +1,15 @@ +\* Full system: ChainScanNext (Bitcoin-poll) racing against GoatRaceNext +\* (GoatChain L2-event-watcher), matching the real code where both are +\* independently-scheduled tasks writing the same `graph.status` column +\* with no coordination. Expected result: NoConflictingWithdrawal, +\* TerminalStatusesAreAbsorbing and DisproveIsSticky FAIL - that failure +\* is the bug this spec exists to demonstrate, not a spec error. +SPECIFICATION FairFullSpec + +CHECK_DEADLOCK FALSE + +INVARIANT TypeOK + +PROPERTY NoConflictingWithdrawal +PROPERTY DisproveIsSticky +PROPERTY TerminalStatusesAreAbsorbing diff --git a/node/tla/GraphLifecycle.tla b/node/tla/GraphLifecycle.tla new file mode 100644 index 00000000..cfceac54 --- /dev/null +++ b/node/tla/GraphLifecycle.tla @@ -0,0 +1,188 @@ +---- MODULE GraphLifecycle ---- +(***************************************************************************) +(* Formal model of a single BitVM2 graph's `status` column, built from the *) +(* ACTUAL Rust implementation (not the README, which was found to be *) +(* stale in several ways - see git history / PR description for details). *) +(* Every action below cites the exact code it models. *) +(* *) +(* Ground truth was established by fully reading: *) +(* - scan_graph_chain_state, node/src/utils.rs:1328-1681 (Bitcoin-poll *) +(* status derivation, the "official" state machine) *) +(* - handle_withdraw_paths_events / handle_withdraw_disproved_events, *) +(* node/src/scheduled_tasks/event_watch_task.rs:392-502 (GoatChain *) +(* L2-event-driven status writes - a SEPARATE, independently *) +(* scheduled writer to the same column) *) +(* - StorageProcessor::update_graph / update_graph_status, *) +(* crates/store/src/localdb.rs:1290-1297 and node/src/utils.rs: *) +(* 5185-5223 (both perform a raw `UPDATE graph SET status=?` with NO *) +(* guard against the new status being older/inconsistent with the *) +(* current one - confirmed by reading both implementations) *) +(* *) +(* Known deliberate simplifications (do not affect the properties below): *) +(* - OperatorPresigned -> CommitteePresigned is modeled as one atomic *) +(* action. In reality it fires once an N-of-N committee-signature *) +(* quorum completes (node/src/action.rs:488-540, try_finalize_graph); *) +(* quorum counting across individual committee members is out of *) +(* scope for a single-graph spec. *) +(* - The four distinct Challenge-state disprove detectors (guardian, *) +(* watchtower-flow, per-verifier, connector-d/PubinDisprove - all in *) +(* utils.rs:1490-1670) are collapsed into one `Disprove` action, since *) +(* they differ only in `disprove_type`/`disprove_index` bookkeeping, *) +(* which never gates any further `status` transition (confirmed: the *) +(* ChallengeSubStatus fields and its is_watchtower_challenge_success *) +(* method have zero call sites anywhere in the repo - dead code). *) +(***************************************************************************) +EXTENDS GraphTopology + +VARIABLE status + +vars == <> + +TypeOK == status \in AllStatuses + +Init == status = "OperatorPresigned" + +-------------------------------------------------------------------------- +(* ChainScan actions: the Bitcoin/GoatChain-poll state machine, *) +(* scan_graph_chain_state, utils.rs:1328-1681. This is the ONLY writer *) +(* considered by the CORE-ONLY config. *) + +CommitteePresign == + /\ status = "OperatorPresigned" + /\ status' = "CommitteePresigned" + +OperatorPushL2Data == + /\ status = "CommitteePresigned" + /\ status' = "OperatorDataPushed" \* utils.rs:1368-1373 + +BecomeObsoletedBeforeData == \* utils.rs:1386-1414 + /\ status \in {"OperatorPresigned", "CommitteePresigned"} + /\ status' = "Obsoleted" + +BecomeObsoletedAfterData == \* utils.rs:1375-1384 + /\ status = "OperatorDataPushed" + /\ status' = "Obsoleted" + +ConfirmPreKickoff == \* utils.rs:1386-1405 + /\ status = "OperatorDataPushed" + /\ status' = "PreKickoff" + +\* utils.rs:1418-1434 - fires from PreKickoff OR Obsoleted (resurrection). +BroadcastKickoff == + /\ status \in {"PreKickoff", "Obsoleted"} + /\ status' = "OperatorKickOff" + +ForceSkip == \* utils.rs:1418-1434 + /\ status \in {"PreKickoff", "Obsoleted"} + /\ status' = "Skipped" + +Take1 == \* utils.rs:1462-1489 + /\ status = "OperatorKickOff" + /\ status' = "OperatorTake1" + +OpenChallenge == \* utils.rs:1462-1489 + /\ status = "OperatorKickOff" + /\ status' = "Challenge" + +\* utils.rs:1448-1461 - guardian disprove bypasses Challenge entirely; this +\* edge is absent from the README's Fig-04-1/4-2 diagram. +GuardianDisproveFromKickoff == + /\ status = "OperatorKickOff" + /\ status' = "Disprove" + +Take2 == \* utils.rs:1661-1670 + /\ status = "Challenge" + /\ status' = "OperatorTake2" + +\* Collapses all 4 real disprove detectors - see header comment. +Disprove == \* utils.rs:1490-1660 + /\ status = "Challenge" + /\ status' = "Disprove" + +ChainScanNext == + \/ CommitteePresign + \/ OperatorPushL2Data + \/ BecomeObsoletedBeforeData + \/ BecomeObsoletedAfterData + \/ ConfirmPreKickoff + \/ BroadcastKickoff + \/ ForceSkip + \/ Take1 + \/ OpenChallenge + \/ GuardianDisproveFromKickoff + \/ Take2 + \/ Disprove + +-------------------------------------------------------------------------- +(* GoatRace actions: node/src/scheduled_tasks/event_watch_task.rs, a *) +(* SEPARATE scheduled task that reacts to GoatChain (L2) events and *) +(* writes `status` via StorageProcessor::update_graph - a raw SQL UPDATE *) +(* with no WHERE-clause guard on the current status *) +(* (crates/store/src/localdb.rs:1290-1297) and no coordination with *) +(* ChainScanNext above. Modeled exactly as read: no precondition on the *) +(* current status at all, matching "any -> X" in the real code. *) + +GoatPostGraphData == status' = "OperatorDataPushed" \* event_watch_task.rs:930-947 +GoatWithdrawHappy == status' = "OperatorTake1" \* event_watch_task.rs:392-449, WithdrawHappyEvent +GoatWithdrawUnhappy == status' = "OperatorTake2" \* event_watch_task.rs:392-449, WithdrawUnhappyEvent +GoatWithdrawDisproved == status' = "Disprove" \* event_watch_task.rs:451-502 + +GoatRaceNext == + \/ GoatPostGraphData + \/ GoatWithdrawHappy + \/ GoatWithdrawUnhappy + \/ GoatWithdrawDisproved + +\* The actual fix (update_graph_status_guarded, node/src/utils.rs) has TWO +\* guards, not one - a first draft with only guard (1) still let TLC find a +\* liveness bug: GoatPostGraphData could still knock an OperatorKickOff graph +\* back to OperatorDataPushed, which combined with Obsoleted's resurrection +\* edges cycles forever and never reaches a terminal status. +\* (1) refuse to write OFF a closed/terminal status onto something else. +\* (2) refuse OperatorDataPushed specifically unless the graph hasn't yet +\* progressed past it (OperatorDataPushed only causally precedes +\* PreKickoff in the real system - a correctly functioning node never +\* needs to "re-push" data after kickoff has already happened). +GoatRaceNextGuarded == + \/ (status \notin TerminalStatuses + /\ status \in {"OperatorPresigned", "CommitteePresigned", "OperatorDataPushed"} + /\ GoatPostGraphData) + \/ (status \notin TerminalStatuses /\ GoatWithdrawHappy) + \/ (status \notin TerminalStatuses /\ GoatWithdrawUnhappy) + \/ (status \notin TerminalStatuses /\ GoatWithdrawDisproved) + +-------------------------------------------------------------------------- +Next == ChainScanNext \/ GoatRaceNext +NextFixed == ChainScanNext \/ GoatRaceNextGuarded + +CoreSpec == Init /\ [][ChainScanNext]_vars +FairCoreSpec == CoreSpec /\ WF_vars(ChainScanNext) + +FullSpec == Init /\ [][Next]_vars +FairFullSpec == FullSpec /\ WF_vars(Next) + +FullSpecFixed == Init /\ [][NextFixed]_vars +FairFullSpecFixed == FullSpecFixed /\ WF_vars(NextFixed) + +-------------------------------------------------------------------------- +(* Properties. Checked against FairCoreSpec (GraphLifecycleCoreOnly.cfg, *) +(* expected to hold - the chain-scan state machine alone is sound) and *) +(* against FairFullSpec (GraphLifecycle.cfg, expected to VIOLATE - that *) +(* is the actual bug this spec was built to demonstrate). *) + +NoStatusSkipping == [][(status # status' => <> \in AllowedTransitions)]_vars + +TerminalStatusesAreAbsorbing == [][(status \in TerminalStatuses => status' = status)]_vars + +\* The sharpest statement of the real risk: the two payout paths (happy-path +\* Take1 vs challenge-path Take2) must never both be recorded for one graph, +\* and a recorded Disprove (evidence an operator cheated) must never be lost. +NoConflictingWithdrawal == + /\ [][(status = "OperatorTake1" => status' # "OperatorTake2")]_vars + /\ [][(status = "OperatorTake2" => status' # "OperatorTake1")]_vars + +DisproveIsSticky == [][(status = "Disprove" => status' = "Disprove")]_vars + +EventuallyTerminal == <>(status \in TerminalStatuses) + +==== diff --git a/node/tla/GraphLifecycleCoreOnly.cfg b/node/tla/GraphLifecycleCoreOnly.cfg new file mode 100644 index 00000000..e47d7efb --- /dev/null +++ b/node/tla/GraphLifecycleCoreOnly.cfg @@ -0,0 +1,15 @@ +\* Baseline: the chain-scan state machine alone (ChainScanNext), no GoatChain +\* event-watcher race. Expected result: all properties hold. This isolates +\* whether the CORE lifecycle logic is sound by itself, so that any failure +\* in GraphLifecycle.cfg can be attributed specifically to the second writer. +SPECIFICATION FairCoreSpec + +CHECK_DEADLOCK FALSE + +INVARIANT TypeOK + +PROPERTY NoStatusSkipping +PROPERTY TerminalStatusesAreAbsorbing +PROPERTY NoConflictingWithdrawal +PROPERTY DisproveIsSticky +PROPERTY EventuallyTerminal diff --git a/node/tla/GraphLifecycleFineGrained.cfg b/node/tla/GraphLifecycleFineGrained.cfg new file mode 100644 index 00000000..90e47189 --- /dev/null +++ b/node/tla/GraphLifecycleFineGrained.cfg @@ -0,0 +1,5 @@ +SPECIFICATION Spec + +INVARIANT TypeOK +PROPERTY TerminalStatusesAreAbsorbing +PROPERTY NoConflictingWithdrawal diff --git a/node/tla/GraphLifecycleFineGrained.tla b/node/tla/GraphLifecycleFineGrained.tla new file mode 100644 index 00000000..08f0bb31 --- /dev/null +++ b/node/tla/GraphLifecycleFineGrained.tla @@ -0,0 +1,125 @@ +---- MODULE GraphLifecycleFineGrained ---- +(***************************************************************************) +(* GraphLifecycle.tla / GraphLifecycleFixed.cfg model update_graph_status_guarded's *) +(* guard-check-then-write as ONE atomic TLA+ step. The real function is NOT *) +(* atomic: `find_graph(...).await` (the read) and `update_graph(...).await` *) +(* (the write) are two separate yield points (node/src/utils.rs). This *) +(* module exposes that gap explicitly with PlusCal: each writer reads a *) +(* snapshot, decides using the SAME guard logic as the real fix, and only *) +(* writes later - so TLC can explore another writer's full read-decide- *) +(* write completing in between. *) +(***************************************************************************) +EXTENDS GraphTopology + +GoatTargets == {"OperatorDataPushed", "OperatorTake1", "OperatorTake2", "Disprove"} + +(* The exact guard from node/src/utils.rs's update_graph_status_guarded, + applied to a (possibly stale) `current` reading. *) +GuardOK(current, target) == + /\ ~(current \in TerminalStatuses /\ current # target) + /\ (target = "OperatorDataPushed" => + current \in {"OperatorPresigned", "CommitteePresigned", "OperatorDataPushed"}) + +(*--algorithm GraphWriteRace +variables status = "OperatorPresigned"; + +process ChainScan = "ChainScan" +variables csSnap = "", csTarget = ""; +begin + CSRead: + while TRUE do + csSnap := status; + with t \in ({t2 \in AllStatuses : <> \in AllowedTransitions} \cup {csSnap}) do + csTarget := t; + end with; + CSWrite: + if GuardOK(csSnap, csTarget) then + status := csTarget; + end if; + end while; +end process; + +process GoatRace = "GoatRace" +variables grSnap = "", grTarget = ""; +begin + GRRead: + while TRUE do + grSnap := status; + with t \in GoatTargets do + grTarget := t; + end with; + GRWrite: + if GuardOK(grSnap, grTarget) then + status := grTarget; + end if; + end while; +end process; + +end algorithm; *) +\* BEGIN TRANSLATION +VARIABLES status, pc, csSnap, csTarget, grSnap, grTarget + +vars == << status, pc, csSnap, csTarget, grSnap, grTarget >> + +ProcSet == {"ChainScan"} \cup {"GoatRace"} + +Init == (* Global variables *) + /\ status = "OperatorPresigned" + (* Process ChainScan *) + /\ csSnap = "" + /\ csTarget = "" + (* Process GoatRace *) + /\ grSnap = "" + /\ grTarget = "" + /\ pc = [self \in ProcSet |-> CASE self = "ChainScan" -> "CSRead" + [] self = "GoatRace" -> "GRRead"] + +CSRead == /\ pc["ChainScan"] = "CSRead" + /\ csSnap' = status + /\ \E t \in ({t2 \in AllStatuses : <> \in AllowedTransitions} \cup {csSnap'}): + csTarget' = t + /\ pc' = [pc EXCEPT !["ChainScan"] = "CSWrite"] + /\ UNCHANGED << status, grSnap, grTarget >> + +CSWrite == /\ pc["ChainScan"] = "CSWrite" + /\ IF GuardOK(csSnap, csTarget) + THEN /\ status' = csTarget + ELSE /\ TRUE + /\ UNCHANGED status + /\ pc' = [pc EXCEPT !["ChainScan"] = "CSRead"] + /\ UNCHANGED << csSnap, csTarget, grSnap, grTarget >> + +ChainScan == CSRead \/ CSWrite + +GRRead == /\ pc["GoatRace"] = "GRRead" + /\ grSnap' = status + /\ \E t \in GoatTargets: + grTarget' = t + /\ pc' = [pc EXCEPT !["GoatRace"] = "GRWrite"] + /\ UNCHANGED << status, csSnap, csTarget >> + +GRWrite == /\ pc["GoatRace"] = "GRWrite" + /\ IF GuardOK(grSnap, grTarget) + THEN /\ status' = grTarget + ELSE /\ TRUE + /\ UNCHANGED status + /\ pc' = [pc EXCEPT !["GoatRace"] = "GRRead"] + /\ UNCHANGED << csSnap, csTarget, grSnap, grTarget >> + +GoatRace == GRRead \/ GRWrite + +Next == ChainScan \/ GoatRace + +Spec == Init /\ [][Next]_vars + +\* END TRANSLATION + +TypeOK == status \in AllStatuses + +TerminalStatusesAreAbsorbing == [][(status \in TerminalStatuses => status' = status)]_status + +NoConflictingWithdrawal == + /\ [][(status = "OperatorTake1" => status' # "OperatorTake2")]_status + /\ [][(status = "OperatorTake2" => status' # "OperatorTake1")]_status + +==== diff --git a/node/tla/GraphLifecycleFineGrainedFixed.cfg b/node/tla/GraphLifecycleFineGrainedFixed.cfg new file mode 100644 index 00000000..b39fd13e --- /dev/null +++ b/node/tla/GraphLifecycleFineGrainedFixed.cfg @@ -0,0 +1,4 @@ +SPECIFICATION Spec +INVARIANT TypeOK +PROPERTY TerminalStatusesAreAbsorbing +PROPERTY NoConflictingWithdrawal diff --git a/node/tla/GraphLifecycleFineGrainedFixed.tla b/node/tla/GraphLifecycleFineGrainedFixed.tla new file mode 100644 index 00000000..0fe4c5b7 --- /dev/null +++ b/node/tla/GraphLifecycleFineGrainedFixed.tla @@ -0,0 +1,107 @@ +---- MODULE GraphLifecycleFineGrainedFixed ---- +(***************************************************************************) +(* Companion to GraphLifecycleFineGrained.tla, which demonstrated that *) +(* splitting the guard-check from the write into two separate steps (read, *) +(* then later write - exactly what a `SELECT` followed by an `UPDATE` in *) +(* application code does) leaves a real gap, even with the correct guard *) +(* logic on both sides. *) +(* *) +(* The actual fix (node/src/utils.rs + crates/store/src/localdb.rs) moves *) +(* the guard into the SQL statement itself: `GraphUpdate::only_if_status_in` *) +(* becomes a `WHERE status IN (...)` clause on the same `UPDATE` that sets *) +(* the new status, so the read-the-current-value-and-decide step and the *) +(* write happen as ONE indivisible database statement - there is no gap *) +(* for another writer's statement to land inside. *) +(* *) +(* This module models exactly that: each writer's read+decide+write is ONE *) +(* PlusCal label (one atomic step), reusing the identical guard and target *) +(* sets from GraphLifecycleFineGrained.tla. If the properties that failed *) +(* there hold here, that confirms the SQL-level fix - not just the *) +(* application-level guard logic - is what actually closes the gap. *) +(***************************************************************************) +EXTENDS GraphTopology + +GoatTargets == {"OperatorDataPushed", "OperatorTake1", "OperatorTake2", "Disprove"} + +GuardOK(current, target) == + /\ ~(current \in TerminalStatuses /\ current # target) + /\ (target = "OperatorDataPushed" => + current \in {"OperatorPresigned", "CommitteePresigned", "OperatorDataPushed"}) + +(*--algorithm GraphWriteRaceFixed +variables status = "OperatorPresigned"; + +process ChainScan = "ChainScan" +variables csTarget = ""; +begin + CSStep: + while TRUE do + with t \in ({t2 \in AllStatuses : <> \in AllowedTransitions} \cup {status}) do + csTarget := t; + end with; + if GuardOK(status, csTarget) then + status := csTarget; + end if; + end while; +end process; + +process GoatRace = "GoatRace" +variables grTarget = ""; +begin + GRStep: + while TRUE do + with t \in GoatTargets do + grTarget := t; + end with; + if GuardOK(status, grTarget) then + status := grTarget; + end if; + end while; +end process; + +end algorithm; *) +\* BEGIN TRANSLATION +VARIABLES status, csTarget, grTarget + +vars == << status, csTarget, grTarget >> + +ProcSet == {"ChainScan"} \cup {"GoatRace"} + +Init == (* Global variables *) + /\ status = "OperatorPresigned" + (* Process ChainScan *) + /\ csTarget = "" + (* Process GoatRace *) + /\ grTarget = "" + +ChainScan == /\ \E t \in ({t2 \in AllStatuses : <> \in AllowedTransitions} \cup {status}): + csTarget' = t + /\ IF GuardOK(status, csTarget') + THEN /\ status' = csTarget' + ELSE /\ TRUE + /\ UNCHANGED status + /\ UNCHANGED grTarget + +GoatRace == /\ \E t \in GoatTargets: + grTarget' = t + /\ IF GuardOK(status, grTarget') + THEN /\ status' = grTarget' + ELSE /\ TRUE + /\ UNCHANGED status + /\ UNCHANGED csTarget + +Next == ChainScan \/ GoatRace + +Spec == Init /\ [][Next]_vars + +\* END TRANSLATION + +TypeOK == status \in AllStatuses + +TerminalStatusesAreAbsorbing == [][(status \in TerminalStatuses => status' = status)]_status + +NoConflictingWithdrawal == + /\ [][(status = "OperatorTake1" => status' # "OperatorTake2")]_status + /\ [][(status = "OperatorTake2" => status' # "OperatorTake1")]_status + +==== diff --git a/node/tla/GraphLifecycleFixed.cfg b/node/tla/GraphLifecycleFixed.cfg new file mode 100644 index 00000000..a94b2376 --- /dev/null +++ b/node/tla/GraphLifecycleFixed.cfg @@ -0,0 +1,16 @@ +\* Same full system as GraphLifecycle.cfg (ChainScanNext racing the GoatChain +\* event-watcher), but with the actual fix applied: GoatRaceNextGuarded +\* mirrors update_graph_status_guarded's closed-status check +\* (node/src/utils.rs). Expected result: all properties now hold - this +\* config is the proof that the code fix closes the gap GraphLifecycle.cfg +\* found, not just a claim. +SPECIFICATION FairFullSpecFixed + +CHECK_DEADLOCK FALSE + +INVARIANT TypeOK + +PROPERTY NoConflictingWithdrawal +PROPERTY DisproveIsSticky +PROPERTY TerminalStatusesAreAbsorbing +PROPERTY EventuallyTerminal diff --git a/node/tla/GraphTopology.tla b/node/tla/GraphTopology.tla new file mode 100644 index 00000000..0f792802 --- /dev/null +++ b/node/tla/GraphTopology.tla @@ -0,0 +1,46 @@ +---- MODULE GraphTopology ---- +(***************************************************************************) +(* The real Graph.status state machine, shared by every spec that models *) +(* it (GraphLifecycle.tla, GraphLifecycleFineGrained.tla, *) +(* GraphLifecycleFineGrainedFixed.tla) - a single source of truth for the *) +(* status set and the real transition edge set, instead of three *) +(* independent copies that could silently drift out of sync with each *) +(* other and with the real code. *) +(* *) +(* Ground truth: scan_graph_chain_state, node/src/utils.rs:1328-1681. *) +(***************************************************************************) + +AllStatuses == { + "OperatorPresigned", "CommitteePresigned", "OperatorDataPushed", + "PreKickoff", "OperatorKickOff", "Challenge", + "OperatorTake1", "OperatorTake2", "Skipped", "Obsoleted", "Disprove" +} + +\* Obsoleted is NOT terminal in the real code. utils.rs:1418-1434 re-checks +\* `matches!(current_status, PreKickoff | Obsoleted)` and can move an +\* Obsoleted graph to OperatorKickOff or Skipped if a kickoff tx is later +\* observed on-chain. +TerminalStatuses == {"OperatorTake1", "OperatorTake2", "Skipped", "Disprove"} + +\* The real edge set from scan_graph_chain_state - used directly by +\* GraphLifecycleCoreOnly.cfg to check the chain-scan state machine is +\* internally sound in isolation, and by NoStatusSkipping in GraphLifecycle.tla. +AllowedTransitions == { + <<"OperatorPresigned", "CommitteePresigned">>, + <<"CommitteePresigned", "OperatorDataPushed">>, + <<"OperatorDataPushed", "PreKickoff">>, + <<"OperatorPresigned", "Obsoleted">>, + <<"CommitteePresigned", "Obsoleted">>, + <<"OperatorDataPushed", "Obsoleted">>, + <<"PreKickoff", "OperatorKickOff">>, + <<"Obsoleted", "OperatorKickOff">>, + <<"PreKickoff", "Skipped">>, + <<"Obsoleted", "Skipped">>, + <<"OperatorKickOff", "OperatorTake1">>, + <<"OperatorKickOff", "Challenge">>, + <<"OperatorKickOff", "Disprove">>, + <<"Challenge", "OperatorTake2">>, + <<"Challenge", "Disprove">> +} + +==== diff --git a/node/tla/InstanceBridgeOutRace.cfg b/node/tla/InstanceBridgeOutRace.cfg new file mode 100644 index 00000000..78e41007 --- /dev/null +++ b/node/tla/InstanceBridgeOutRace.cfg @@ -0,0 +1,6 @@ +\* Models the CURRENT, actual code (all writers unconditional) - expected +\* to FAIL. This is a live bug, not a historical artifact. +SPECIFICATION FairSpec +CHECK_DEADLOCK FALSE +INVARIANT TypeOK +PROPERTY TerminalStatusesAreAbsorbing diff --git a/node/tla/InstanceBridgeOutRace.tla b/node/tla/InstanceBridgeOutRace.tla new file mode 100644 index 00000000..febc366d --- /dev/null +++ b/node/tla/InstanceBridgeOutRace.tla @@ -0,0 +1,80 @@ +---- MODULE InstanceBridgeOutRace ---- +(***************************************************************************) +(* Formal model of a race found while auditing every remaining stateful *) +(* enum after GraphStatus/InstanceBridgeInStatus (see audit/TLAPlus-*.md). *) +(* *) +(* `InstanceBridgeOutStatus` (crates/store/src/schema.rs:223-229) is *) +(* written from three independently-scheduled, uncoordinated tasks with *) +(* no shared transaction spanning read+decide+write in any of them: *) +(* - the RPC-service task (node/src/rpc_service/handler/bitvm2_handler.rs, *) +(* `bridge_out_init_tag`) - stale-read-then-full-row-upsert, sets *) +(* Initialize. *) +(* - the GoatChain L2-event watcher (node/src/scheduled_tasks/ *) +(* event_watch_task.rs), 5s 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 - no `with_only_if_status_in` exists *) +(* anywhere in the codebase for this entity). *) +(* - the maintenance task (node/src/scheduled_tasks/ *) +(* instance_maintenance_tasks.rs, `instance_bridge_out_monitor`), 10s *) +(* 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. *) +(* *) +(* Modeled the same way GraphLifecycle.tla models its GoatRace actions: *) +(* every writer is unconditional on the CURRENT status - that's the *) +(* verified real behavior, not a simplification of it. *) +(***************************************************************************) +Statuses == {"Initialize", "Claim", "Timeout", "Refund"} + +\* Once a bridge-out instance is claimed, timed out, or refunded, that is +\* meant to be the final outcome - Initialize is the only non-terminal +\* status. +TerminalStatuses == {"Claim", "Timeout", "Refund"} + +VARIABLE status +vars == <> + +TypeOK == status \in Statuses + +Init == status = "Initialize" + +-------------------------------------------------------------------------- +(* Every action below is unconditional on the current status - confirmed *) +(* real behavior for all three writers, not a modeling simplification. *) + +RpcStaleInit == status' = "Initialize" \* bitvm2_handler.rs bridge_out_init_tag, stale full-row upsert +WatchEventClaim == status' = "Claim" \* event_watch_task.rs handle_swap_claim_events +WatchEventRefund == status' = "Refund" \* event_watch_task.rs handle_swap_refund_events +MaintenanceTimeout == status' = "Timeout" \* instance_maintenance_tasks.rs instance_bridge_out_monitor + +Next == + \/ RpcStaleInit + \/ WatchEventClaim + \/ WatchEventRefund + \/ MaintenanceTimeout + +Spec == Init /\ [][Next]_vars +FairSpec == Spec /\ WF_vars(Next) + +\* Proposed fix design (not applied to code - same atomic-CAS pattern as +\* the GraphStatus/InstanceBridgeInStatus fixes: only write if the row +\* isn't already in a terminal status, folded into the UPDATE's WHERE +\* clause so there's no read-then-write gap). +NextFixed == + \/ (status \notin TerminalStatuses /\ RpcStaleInit) + \/ (status \notin TerminalStatuses /\ WatchEventClaim) + \/ (status \notin TerminalStatuses /\ WatchEventRefund) + \/ (status \notin TerminalStatuses /\ MaintenanceTimeout) + +SpecFixed == Init /\ [][NextFixed]_vars +FairSpecFixed == SpecFixed /\ WF_vars(NextFixed) + +-------------------------------------------------------------------------- +(* Safety property: once a bridge-out instance reaches a final outcome, *) +(* it never changes again - e.g. a successfully Claimed instance must *) +(* never be silently reset by a stale maintenance-task Timeout write. *) +TerminalStatusesAreAbsorbing == [][(status \in TerminalStatuses => status' = status)]_status + +==== diff --git a/node/tla/InstanceBridgeOutRaceFixed.cfg b/node/tla/InstanceBridgeOutRaceFixed.cfg new file mode 100644 index 00000000..ffdbda46 --- /dev/null +++ b/node/tla/InstanceBridgeOutRaceFixed.cfg @@ -0,0 +1,5 @@ +\* Proposed fix design (verified, NOT applied to code) - expected to pass. +SPECIFICATION FairSpecFixed +CHECK_DEADLOCK FALSE +INVARIANT TypeOK +PROPERTY TerminalStatusesAreAbsorbing diff --git a/node/tla/InstancePresigned.tla b/node/tla/InstancePresigned.tla new file mode 100644 index 00000000..b984c045 --- /dev/null +++ b/node/tla/InstancePresigned.tla @@ -0,0 +1,76 @@ +---- MODULE InstancePresigned ---- +(***************************************************************************) +(* Formal model of the SECOND race found by the same audit that produced *) +(* GraphLifecycle.tla: `Instance.status` (InstanceBridgeInStatus, *) +(* crates/store/src/schema.rs:194-217) has NO terminal-status protection *) +(* anywhere in the codebase (confirmed: `grep -n "impl InstanceBridgeInStatus"` *) +(* has zero hits, unlike GraphStatus which has is_closed()). *) +(* *) +(* This is deliberately NOT a full Instance-lifecycle spec - InstanceBridgeInStatus's *) +(* full transition graph has not been ground-truth-traced with the same *) +(* rigor GraphLifecycle.tla's was. It models exactly the one confirmed *) +(* regression: `store_graph` (node/src/utils.rs, ~4438) and *) +(* `update_graph_status_guarded` (node/src/utils.rs, ~5195) both write *) +(* `InstanceBridgeInStatus::Presigned` unconditionally as a side effect of *) +(* a graph reaching `CommitteePresigned`, reachable from independent P2P- *) +(* message handlers (handle.rs) and chain-rescans (graph_maintenance_tasks) *) +(* with no coordination - so a stale/replayed event could revert an *) +(* instance that already progressed past Presigned (e.g. to *) +(* RelayerL1Broadcasted or RelayerL2Minted, written by instance_btc_tx_monitor *) +(* and handle_bridge_in_events respectively) back down to Presigned. *) +(* *) +(* "Advanced" below is an abstract stand-in for every real status that *) +(* causally follows Presigned (RelayerL1Broadcasted, RelayerL2Minted, *) +(* RelayerL2MintedFailed, PresignedFailed, Timeout, UserCanceled, *) +(* UserDiscarded, NoEnoughCommitteesAnswered) - the guard treats all of *) +(* them identically (refuse), so collapsing them loses no precision for *) +(* the property being checked here. *) +(***************************************************************************) + +Statuses == {"Early", "Presigned", "Advanced"} + +VARIABLE status +vars == <> + +TypeOK == status \in Statuses + +Init == status = "Early" + +-------------------------------------------------------------------------- +(* The real, legitimate forward progression (instance_btc_tx_monitor, *) +(* handle_bridge_in_events, etc. - the various writers that correctly *) +(* advance status once real on-chain/L2 events are observed). *) + +AdvancePastPresigned == + /\ status = "Presigned" + /\ status' = "Advanced" + +-------------------------------------------------------------------------- +(* The race: store_graph / update_graph_status_guarded writing Presigned *) +(* as a side effect, reachable from an independent P2P/rescan path. *) + +\* As originally written: unconditional, matching the confirmed bug. +SetPresignedUnguarded == + status' = "Presigned" + +\* The actual fix: set_instance_presigned_guarded, node/src/utils.rs - +\* only writes Presigned if the instance hasn't already moved past it. +SetPresignedGuarded == + /\ status \in {"Early", "Presigned"} + /\ status' = "Presigned" + +-------------------------------------------------------------------------- +NextBug == AdvancePastPresigned \/ SetPresignedUnguarded +NextFixed == AdvancePastPresigned \/ SetPresignedGuarded + +SpecBug == Init /\ [][NextBug]_vars +SpecFixed == Init /\ [][NextFixed]_vars + +-------------------------------------------------------------------------- +(* The property: once an instance is Advanced, it must never regress. This *) +(* is deliberately not "terminal is absorbing" in the GraphStatus sense - *) +(* Advanced isn't necessarily terminal in reality, it just must never be *) +(* undone by the Presigned side-effect write specifically. *) +NeverRegressPastPresigned == [][(status = "Advanced" => status' = "Advanced")]_vars + +==== diff --git a/node/tla/InstancePresignedBug.cfg b/node/tla/InstancePresignedBug.cfg new file mode 100644 index 00000000..d701c5dd --- /dev/null +++ b/node/tla/InstancePresignedBug.cfg @@ -0,0 +1,4 @@ +SPECIFICATION SpecBug +CHECK_DEADLOCK FALSE +INVARIANT TypeOK +PROPERTY NeverRegressPastPresigned diff --git a/node/tla/InstancePresignedFixed.cfg b/node/tla/InstancePresignedFixed.cfg new file mode 100644 index 00000000..4aaf1854 --- /dev/null +++ b/node/tla/InstancePresignedFixed.cfg @@ -0,0 +1,4 @@ +SPECIFICATION SpecFixed +CHECK_DEADLOCK FALSE +INVARIANT TypeOK +PROPERTY NeverRegressPastPresigned diff --git a/node/tla/MessageStateRace.cfg b/node/tla/MessageStateRace.cfg new file mode 100644 index 00000000..13a129bf --- /dev/null +++ b/node/tla/MessageStateRace.cfg @@ -0,0 +1,6 @@ +\* Models the CURRENT, actual code (upsert_message resurrects unconditionally) +\* - expected to FAIL. This is a live bug, not a historical artifact. +SPECIFICATION FairSpec +CHECK_DEADLOCK FALSE +INVARIANT TypeOK +PROPERTY TerminalStatusesAreAbsorbing diff --git a/node/tla/MessageStateRace.tla b/node/tla/MessageStateRace.tla new file mode 100644 index 00000000..9ab76eab --- /dev/null +++ b/node/tla/MessageStateRace.tla @@ -0,0 +1,78 @@ +---- MODULE MessageStateRace ---- +(***************************************************************************) +(* Formal model of a race found while auditing every remaining stateful *) +(* enum after GraphStatus/InstanceBridgeInStatus (see audit/TLAPlus-*.md). *) +(* *) +(* `MessageState` (crates/store/src/schema.rs:431-437: Pending, Processed, *) +(* Failed, Expired, Cancelled) tracks P2P message delivery/processing *) +(* status. Unlike the GraphStatus/InstanceBridgeOutStatus findings, the *) +(* "cancel" writer here IS correctly guarded - `update_messages_state_by_ *) +(* business_id` (crates/store/src/localdb.rs) does a real CAS: `UPDATE ... *) +(* WHERE business_id=? AND state='Pending'`, called from *) +(* node/src/scheduled_tasks/event_watch_task.rs's handle_withdraw_paths_/ *) +(* disproved_events when a graph reaches a closed on-chain status *) +(* (OperatorTake1/OperatorTake2/Disprove) - bulk-cancelling any still- *) +(* Pending message for that graph as moot. *) +(* *) +(* The bug is on the OTHER side: `upsert_message` (node/src/utils.rs, *) +(* called by push_local_unhandled_messages - the generic "defer/retry *) +(* this p2p message" primitive used ~30 times across node/src/handle.rs) *) +(* with `is_update=true` unconditionally sets state back to Pending via *) +(* `INSERT ... ON CONFLICT(message_id) DO UPDATE SET state=excluded.state` *) +(* - no WHERE clause is possible on an upsert, so a message the system *) +(* just administratively marked Cancelled (because its graph is already *) +(* finalized) can be silently resurrected to Pending and re-dispatched *) +(* the next time a handler in the swarm-message task calls a retry/defer *) +(* on it, unrelated to the cancellation. *) +(***************************************************************************) +Statuses == {"Pending", "Cancelled"} + +\* Cancelled is an administrative "this message is moot, stop touching it" +\* marker tied to its graph reaching a closed status - it must stay final. +TerminalStatuses == {"Cancelled"} + +VARIABLE status +vars == <> + +TypeOK == status \in Statuses + +Init == status = "Pending" + +-------------------------------------------------------------------------- +\* event_watch_task.rs's handle_withdraw_paths_events / handle_withdraw_ +\* disproved_events, via update_messages_state_by_business_id - a genuine +\* CAS, correctly guarded in the real code. +BulkCancelOnGraphClose == + /\ status = "Pending" + /\ status' = "Cancelled" + +\* push_local_unhandled_messages -> utils::upsert_message(is_update=true) +\* -> store upsert_message's `ON CONFLICT DO UPDATE SET state=excluded.state` +\* - confirmed NO guard of any kind. Fires from ~30 call sites in +\* node/src/handle.rs whenever a message handler needs to defer/retry, +\* with no awareness of whether the message was since cancelled. +ResurrectPendingUnconditional == status' = "Pending" + +Next == + \/ BulkCancelOnGraphClose + \/ ResurrectPendingUnconditional + +Spec == Init /\ [][Next]_vars +FairSpec == Spec /\ WF_vars(Next) + +\* Proposed fix design (not applied to code): guard the resurrect-to-Pending +\* write the same way - only apply it if the message isn't already in a +\* terminal status, folded into the UPDATE/upsert's WHERE clause. +NextFixed == + \/ BulkCancelOnGraphClose + \/ (status \notin TerminalStatuses /\ ResurrectPendingUnconditional) + +SpecFixed == Init /\ [][NextFixed]_vars +FairSpecFixed == SpecFixed /\ WF_vars(NextFixed) + +-------------------------------------------------------------------------- +\* Safety property: once a message is administratively Cancelled, it must +\* never be resurrected and re-dispatched. +TerminalStatusesAreAbsorbing == [][(status \in TerminalStatuses => status' = status)]_status + +==== diff --git a/node/tla/MessageStateRaceFixed.cfg b/node/tla/MessageStateRaceFixed.cfg new file mode 100644 index 00000000..ffdbda46 --- /dev/null +++ b/node/tla/MessageStateRaceFixed.cfg @@ -0,0 +1,5 @@ +\* Proposed fix design (verified, NOT applied to code) - expected to pass. +SPECIFICATION FairSpecFixed +CHECK_DEADLOCK FALSE +INVARIANT TypeOK +PROPERTY TerminalStatusesAreAbsorbing diff --git a/node/tla/MultiActorRace.cfg b/node/tla/MultiActorRace.cfg new file mode 100644 index 00000000..ae064dfc --- /dev/null +++ b/node/tla/MultiActorRace.cfg @@ -0,0 +1,6 @@ +SPECIFICATION Spec +CHECK_DEADLOCK FALSE +INVARIANT WatchtowerAckWindowAlwaysPositive +INVARIANT NackAlwaysBeatsTake2ViaF +INVARIANT CommitTimeoutAlwaysBeatsTake2ViaF +INVARIANT DisproveAlwaysBeatsTake2ViaD diff --git a/node/tla/MultiActorRace.tla b/node/tla/MultiActorRace.tla new file mode 100644 index 00000000..91b76e3a --- /dev/null +++ b/node/tla/MultiActorRace.tla @@ -0,0 +1,114 @@ +---- MODULE MultiActorRace ---- +(***************************************************************************) +(* Take2DisproveRace.tla abstracted "the watchtower" and "the verifier" as *) +(* single actors. The real protocol has N independent watchtowers and M *) +(* independent verifiers (node/tla/Take2DisproveRace.tla's header / *) +(* node/README.md document the single-actor version). This module checks *) +(* whether N=2/M=2 introduces anything the single-actor abstraction *) +(* couldn't see - the concern being: with multiple independent actors *) +(* timing their own actions, could the "any ONE honest actor suffices" *) +(* property (verified structurally true by a dedicated research pass - *) +(* see below) actually fail on the TIMING axis even though it holds *) +(* logically? *) +(* *) +(* Ground truth (verified by reading goat/src/{connectors,transactions}/*.rs *) +(* directly, not assumed): *) +(* - WATCHTOWERS: all N watchtowers' WatchtowerChallengeConnector[i]/ *) +(* AckConnector[i] pairs are outputs of the SAME, single *) +(* WatchtowerChallengeInitTransaction. So every watchtower's challenge *) +(* window AND the operator's ack window for that slot are measured *) +(* from the SAME shared height, regardless of which watchtower acts or *) +(* when. There is no genuine multi-clock complexity on this side - N *) +(* watchtowers share ONE clock. If watchtower i is un-acked, its Nack *) +(* spends the shared ConnectorF leaf1, permanently blocking Take2's *) +(* leaf0 (first-confirmed-wins UTXO semantics) - true 1-of-N by *) +(* construction, confirmed via detect_watchtower_flow_disprove *) +(* (node/src/utils.rs:1299-1326), which returns Disprove on the FIRST *) +(* nack found, no counting/quorum. *) +(* - VERIFIERS: structurally identical EXCEPT each verifier's clock *) +(* genuinely is independent - VerifierAssertTransaction[i] confirms at *) +(* verifier i's own pace (whenever THEY finish detecting fraud), not a *) +(* shared height. This is the one place N/M actually could matter. *) +(***************************************************************************) +EXTENDS Integers, ShippedTimelocks + +VARIABLES + net, + wtChallengeHeight1, wtChallengeHeight2, \* watchtower i challenges at this height within [0, WatchtowerChallenge[net]], or NEVER + verifierDelta1, verifierDelta2 \* verifier i's own independent assert-confirmation gap after OperatorAssert (assertHeight = 0 WLOG), or NEVER + +vars == <> + +NEVER == -1 + +Init == + /\ net \in Networks + /\ wtChallengeHeight1 \in ({NEVER} \cup 0..WatchtowerChallenge[net]) + /\ wtChallengeHeight2 \in ({NEVER} \cup 0..WatchtowerChallenge[net]) + /\ verifierDelta1 \in ({NEVER} \cup 0..MinReactionBlocks[net]) + /\ verifierDelta2 \in ({NEVER} \cup 0..MinReactionBlocks[net]) + +Next == FALSE \* exhaustive check over Init, not a process model - see Take2DisproveRace.tla +Spec == Init /\ [][Next]_vars + +-------------------------------------------------------------------------- +(* Watchtower side: both slots share ONE clock (WatchtowerChallengeInit's *) +(* confirmation, height 0 here), independent of when each watchtower acts. *) + +Challenged(h) == h # NEVER +NackDeadline == OperatorAck[net] \* shared, same for every watchtower slot +Take2ReadyHeightViaF == ConnectorF[net] +OperatorAckWindow(challengeHeight) == NackDeadline - challengeHeight + +\* For every possible timing of both watchtowers - including both waiting +\* until the literal last block of their window - an operator notified the +\* instant a challenge lands still has a strictly positive number of blocks +\* to construct and confirm their Ack before the shared deadline. Checked +\* per watchtower since neither's window depends on the other's timing (no +\* shared clock to contend over on THIS axis). +WatchtowerAckWindowAlwaysPositive == + /\ (Challenged(wtChallengeHeight1) => OperatorAckWindow(wtChallengeHeight1) > 0) + /\ (Challenged(wtChallengeHeight2) => OperatorAckWindow(wtChallengeHeight2) > 0) + +\* If a watchtower's Nack ever becomes available (operator failed to ack in +\* time - the worst case for the operator), it is always available strictly +\* before Take2 could fire via connector_f, independent of the OTHER +\* watchtower's behavior or timing. +NackAlwaysBeatsTake2ViaF == + /\ (Challenged(wtChallengeHeight1) => NackDeadline < Take2ReadyHeightViaF) + /\ (Challenged(wtChallengeHeight2) => NackDeadline < Take2ReadyHeightViaF) + +-------------------------------------------------------------------------- +(* ConnectorF leaf 1 (the "committee blocks Take2" leaf) has TWO *) +(* alternative spenders, not one - confirmed by reading *) +(* goat/src/transactions/watchtower_challenge.rs directly: *) +(* OperatorChallengeNackTransaction (leaf1, checked above via *) +(* NackAlwaysBeatsTake2ViaF) AND OperatorCommitTimeoutTransaction (also *) +(* leaf1, watchtower_challenge.rs:714-747, jointly spending ConnectorE *) +(* leaf1 as its other input). The Rust side already enforces *) +(* operator_commit < connector_f (timelocks.rs's *) +(* ensure_lt("operator_commit", ..., "connector_f", ...)) but - unlike the *) +(* operator_ack/Nack case - that comparison was never independently *) +(* confirmed here using real shipped values. Same shared clock root as *) +(* every other property in this module (WatchtowerChallengeInit's *) +(* confirmation - operator_commit's clock starts there via ConnectorE's *) +(* own construction, symmetric to operator_ack's), and not a multi-actor *) +(* quantity (one operator, one commit-or-not decision per graph, not per *) +(* watchtower), so no new VARIABLE is needed - just the real numbers. *) +CommitTimeoutDeadline == OperatorCommit[net] +CommitTimeoutAlwaysBeatsTake2ViaF == CommitTimeoutDeadline < Take2ReadyHeightViaF + +-------------------------------------------------------------------------- +(* Verifier side: genuinely independent clocks - each verifier's own delta. *) + +DisproveDeadline(delta) == delta + ProverConnector[net] +Take2ReadyHeightViaD == ConnectorD[net] + +\* Symmetric to the watchtower property: EACH verifier's disprove deadline +\* (rooted at THEIR OWN assert height) must beat Take2's connector_d +\* deadline, regardless of the other verifier's independent timing. +DisproveAlwaysBeatsTake2ViaD == + /\ (verifierDelta1 # NEVER => DisproveDeadline(verifierDelta1) < Take2ReadyHeightViaD) + /\ (verifierDelta2 # NEVER => DisproveDeadline(verifierDelta2) < Take2ReadyHeightViaD) + +==== diff --git a/node/tla/ShippedTimelocks.tla b/node/tla/ShippedTimelocks.tla new file mode 100644 index 00000000..563bad5f --- /dev/null +++ b/node/tla/ShippedTimelocks.tla @@ -0,0 +1,33 @@ +---- MODULE ShippedTimelocks ---- +(***************************************************************************) +(* Real per-network timelock values, transcribed from *) +(* crates/bitvm-gc/src/timelocks.rs, shared by every margin-arithmetic *) +(* spec in this directory (Take2DisproveRace.tla, MultiActorRace.tla, *) +(* Take1ChallengeRace.tla) - single source of truth instead of each spec *) +(* re-transcribing its own copy. *) +(* *) +(* As of commit 991faaa ("Dev fix #418"), Finding 4's fix has actually been *) +(* applied to crates/bitvm-gc/src/timelocks.rs - these are now the REAL *) +(* shipped values (re-verified via TLC against these exact numbers, not *) +(* just the originally-proposed 35/testnet4 figure this file used to hold *) +(* before the real fix landed with a wider margin than proposed). *) +(* connector_a is NOT here - Take1ChallengeRace.tla defines its own *) +(* ConnectorA/ConnectorAFixed locally, since that value pair is the *) +(* historical subject under test in that spec, not a settled shared *) +(* constant. *) +(***************************************************************************) + +Networks == {"Bitcoin", "Testnet4", "Signet", "Regtest"} + +ProverConnector == [Bitcoin |-> 144, Testnet4 |-> 20, Signet |-> 6, Regtest |-> 1] +ConnectorD == [Bitcoin |-> 432, Testnet4 |-> 40, Signet |-> 18, Regtest |-> 3] +WatchtowerChallenge == [Bitcoin |-> 144, Testnet4 |-> 20, Signet |-> 6, Regtest |-> 1] +OperatorAck == [Bitcoin |-> 288, Testnet4 |-> 32, Signet |-> 12, Regtest |-> 2] +OperatorCommit == [Bitcoin |-> 432, Testnet4 |-> 40, Signet |-> 18, Regtest |-> 3] +ConnectorF == [Bitcoin |-> 576, Testnet4 |-> 52, Signet |-> 24, Regtest |-> 4] + +\* Policy floor, not itself a timelocks.rs field: the assumed real-world +\* reaction-time bound (~1 hour) used across every margin-race spec. +MinReactionBlocks == [Bitcoin |-> 6, Testnet4 |-> 12, Signet |-> 1, Regtest |-> 1] + +==== diff --git a/node/tla/Take1ChallengeRace.cfg b/node/tla/Take1ChallengeRace.cfg new file mode 100644 index 00000000..6873f166 --- /dev/null +++ b/node/tla/Take1ChallengeRace.cfg @@ -0,0 +1,6 @@ +\* Models the CURRENT, shipped connector_a values (no margin check exists +\* anywhere in the codebase for this field) - expected to FAIL on at least +\* one network. This is a live gap, not a historical artifact. +SPECIFICATION Spec +CHECK_DEADLOCK FALSE +INVARIANT ChallengeHasSufficientReactionMargin diff --git a/node/tla/Take1ChallengeRace.tla b/node/tla/Take1ChallengeRace.tla new file mode 100644 index 00000000..9c4abd29 --- /dev/null +++ b/node/tla/Take1ChallengeRace.tla @@ -0,0 +1,75 @@ +---- MODULE Take1ChallengeRace ---- +(***************************************************************************) +(* Formal model of a new bottleneck connector found while extending the *) +(* transaction-graph audit beyond ConnectorD (see Take2DisproveRace.tla *) +(* and audit/TLAPlus-*.md's "Cross-check"/connector-sweep follow-up). *) +(* *) +(* Ground truth, read directly from the external goat crate *) +(* (checkout e369b2a, goat/src/connectors/connector_a.rs, *) +(* goat/src/transactions/{kickoff,take1,challenge}.rs): *) +(* - KickoffTransaction creates ConnectorA's output (kickoff.rs:67-71, *) +(* output_0 = connector_a.generate_taproot_address()) - this is the *) +(* single shared clock root for both leaves below. *) +(* - ConnectorA leaf 0: operator key + CSV(connector_a) (connector_a.rs: *) +(* 37-46) - spent by Take1Transaction (take1.rs:73-74, input_1_leaf=0),*) +(* the operator's uncontested fast-exit path. *) +(* - ConnectorA leaf 1: n-of-n committee key, NO CSV at all *) +(* (connector_a.rs:48-54) - spent by ChallengeTransaction *) +(* (challenge.rs:56-57, input_0_leaf=1) using SinglePlusAnyoneCanPay *) +(* (challenge.rs:93), so any third-party challenger can add their own *) +(* fee input and force this tx through the instant it's noticed. *) +(* Same ConnectorA output, different leaves - ordinary UTXO semantics mean *) +(* whichever of Take1/Challenge confirms first wins it permanently. Unlike *) +(* ConnectorD (Take2 vs Disprove), this is a single-clock race: Challenge *) +(* is spendable immediately once Kickoff confirms, so the entire margin a *) +(* challenger has to detect fraud and get Challenge confirmed IS *) +(* connector_a's own CSV value, in blocks, from Kickoff's confirmation. *) +(* *) +(* crates/bitvm-gc/src/timelocks.rs's validate_timelock_config (69-101) *) +(* checks connector_a is merely nonzero (73-81) but - unlike every other *) +(* named timelock field - never puts it in any ensure_lt/ensure_lte *) +(* comparison (91-99). There is no enforcement anywhere in the codebase *) +(* that connector_a leaves a challenger enough real-world reaction time, *) +(* on ANY network, not just the boundary case this module happens to find. *) +(* MinReactionBlocks is the same ~1-hour-of-reaction-time policy floor *) +(* Take2DisproveRace.tla already established and validated for the *) +(* analogous ConnectorD/ProverConnector race - reused here, not re-derived,*) +(* since it's the same real-world assumption (off-chain fraud detection + *) +(* tx construction + broadcast + confirmation lag) applied to a different *) +(* connector. *) +(* *) +(* STATUS UPDATE (commit 991faaa, "Dev fix #418"): this finding has since *) +(* been fixed - validate_timelock_config now has an ensure_gt("connector_a",*) +(* ..., "min_reaction_blocks", ...) check, and NODE_REGTEST_TIMELOCK_CONFIG*) +(* .connector_a was bumped 1 -> 2, exactly matching ConnectorAFixed below. *) +(* ConnectorA (unchanged from this file's original discovery run) is kept *) +(* as a historical record of the exact pre-fix shipped value/counterexample*) +(* - it deliberately does NOT track crates/bitvm-gc/src/timelocks.rs's *) +(* current numbers the way ShippedTimelocks.tla's tables do. *) +(***************************************************************************) +EXTENDS Integers, ShippedTimelocks + +\* Historical: the shipped value at the time this bug was found (pre-fix). +\* Kept as-is rather than updated to track current timelocks.rs - see the +\* STATUS UPDATE note above. +ConnectorA == [Bitcoin |-> 144, Testnet4 |-> 16, Signet |-> 6, Regtest |-> 1] + +\* The fix design, since actually applied verbatim in commit 991faaa. +ConnectorAFixed == [Bitcoin |-> 144, Testnet4 |-> 16, Signet |-> 6, Regtest |-> 2] + +VARIABLE net +vars == <> + +Init == net \in Networks +Next == FALSE \* no transitions - exhaustive check over Init, same idiom as Take2DisproveRace.tla +Spec == Init /\ [][Next]_vars + +\* The property connector_a's margin exists to guarantee but nothing in +\* the code currently checks: a challenger has strictly more than the +\* assumed real-world reaction-time floor, from Kickoff's confirmation, to +\* detect fraud and get a Challenge transaction confirmed before the +\* operator's Take1 CSV (rooted at that same confirmation) elapses. +ChallengeHasSufficientReactionMargin == ConnectorA[net] > MinReactionBlocks[net] +ChallengeHasSufficientReactionMarginFixed == ConnectorAFixed[net] > MinReactionBlocks[net] + +==== diff --git a/node/tla/Take1ChallengeRaceFixed.cfg b/node/tla/Take1ChallengeRaceFixed.cfg new file mode 100644 index 00000000..431c2339 --- /dev/null +++ b/node/tla/Take1ChallengeRaceFixed.cfg @@ -0,0 +1,4 @@ +\* Proposed fix design (verified, NOT applied to code) - expected to pass. +SPECIFICATION Spec +CHECK_DEADLOCK FALSE +INVARIANT ChallengeHasSufficientReactionMarginFixed diff --git a/node/tla/Take2DisproveRace.cfg b/node/tla/Take2DisproveRace.cfg new file mode 100644 index 00000000..78651b87 --- /dev/null +++ b/node/tla/Take2DisproveRace.cfg @@ -0,0 +1,4 @@ +SPECIFICATION Spec +CHECK_DEADLOCK FALSE +INVARIANT DisproveWinsConnectorDRace +INVARIANT ConnectorFNeverShortcutsConnectorD diff --git a/node/tla/Take2DisproveRace.tla b/node/tla/Take2DisproveRace.tla new file mode 100644 index 00000000..1c91cbd4 --- /dev/null +++ b/node/tla/Take2DisproveRace.tla @@ -0,0 +1,83 @@ +---- MODULE Take2DisproveRace ---- +(***************************************************************************) +(* Formal model of the "gap 3" race flagged when auditing timelock *) +(* reasonableness (see crates/bitvm-gc/src/timelocks.rs): whether Take2's *) +(* two-clock readiness condition can be exploited to preempt the *) +(* committee's cooperative Disprove fallback on the same UTXO. *) +(* *) +(* Ground truth, read directly from the external goat crate *) +(* (checkout e369b2a, goat/src/transactions/{take2,assert}.rs): *) +(* - Take2Transaction spends ConnectorD leaf 0 (operator + CSV(connector_d), *) +(* clock starts at OperatorAssert's confirmation) AND ConnectorF leaf 0 *) +(* (operator + CSV(connector_f), clock starts at WatchtowerChallengeInit's *) +(* confirmation) - both required simultaneously (take2.rs:56-90). *) +(* - DisproveTransaction spends ConnectorD leaf 1 (n-of-n, immediate, no *) +(* CSV) AND ProverConnector leaf 1 (n-of-n + CSV(prover_connector), *) +(* clock starts at VerifierAssert's confirmation) (assert.rs:317-357). *) +(* Same ConnectorD output, different leaves - first tx confirmed wins it *) +(* permanently (ordinary Bitcoin UTXO semantics, no special ordering rule *) +(* needed - modeled here as first-deadline-reached-wins). *) +(* *) +(* NOTE (found by the later connector-sweep, audit/TLAPlus-*.md): ConnectorD *) +(* actually has a THIRD leaf (connector_d.rs:20-22,80-88) spent by a free *) +(* function pubin_disprove() (assert.rs:571-599), the node's tracked *) +(* DisproveTxType::PubinDisprove outcome. This module does not model it - *) +(* deliberately, not by oversight: leaf 2's script has NO CSV at all, so it *) +(* can only ever be at least as fast as the already-proven-safe Disprove *) +(* path modeled below, never slower - it cannot introduce a new exploitable *) +(* margin. Noted here so this header no longer implies leaf 2 doesn't exist.*) +(* *) +(* VerifierAssert spends OperatorAssert's output, so it can only confirm *) +(* strictly after OperatorAssert - `delta` below is that real-world gap. *) +(* It is NOT a protocol constant; nothing on-chain bounds it. The fix in *) +(* timelocks.rs assumes it is bounded by min_reaction_blocks(network) (the *) +(* same ~1-hour policy floor used elsewhere). This module checks that *) +(* assumption is sufficient, using the actual shipped per-network values, *) +(* and separately confirms (rather than just hand-argues) that Take2's *) +(* second clock (connector_f, rooted at WatchtowerChallengeInit) can never *) +(* be used to shortcut this specific race, regardless of when the operator *) +(* chooses to broadcast WatchtowerChallengeInit. *) +(***************************************************************************) +EXTENDS Integers, ShippedTimelocks + +Max(a, b) == IF a >= b THEN a ELSE b + +VARIABLES net, delta, wci + +vars == <> + +\* assertHeight is fixed at 0 WLOG - every other height is expressed relative +\* to it. delta ranges up to (and including) the assumed reaction-time bound +\* for each network - this is exactly the guarantee the timelocks.rs fix is +\* supposed to provide, checked here rather than re-derived by hand. wci +\* ranges over a wide symmetric window since the operator fully controls +\* when WatchtowerChallengeInit is broadcast (it is their own signed, +\* immediately-spendable transaction, independent of OperatorAssert - see +\* watchtower_challenge.rs:178-256, both connector_b and connector_c are +\* plain, unrelated Kickoff outputs with no relative ordering between them). +Init == + /\ net \in Networks + /\ delta \in 0..MinReactionBlocks[net] + /\ wci \in -300..300 + +vars_unchanged == UNCHANGED vars +Next == FALSE \* no transitions - this is an exhaustive check over Init, not a process model +Spec == Init /\ [][Next]_vars + +DisproveDeadline == delta + ProverConnector[net] +Take2ConnectorDDeadline == ConnectorD[net] +Take2ConnectorFDeadline == wci + ConnectorF[net] +Take2Deadline == Max(Take2ConnectorDDeadline, Take2ConnectorFDeadline) + +\* The property the margin fix in validate_timelock_config exists to +\* guarantee: within the assumed real-world confirmation-gap bound, Disprove +\* always has a strictly earlier deadline than Take2's ConnectorD leaf, for +\* every choice the operator could make for wci. +DisproveWinsConnectorDRace == DisproveDeadline < Take2ConnectorDDeadline + +\* Independent confirmation (not just a hand-argument) that ConnectorF can +\* only ever add delay to Take2, never let the operator preempt the +\* ConnectorD-specific race early via a clever choice of wci. +ConnectorFNeverShortcutsConnectorD == Take2Deadline >= Take2ConnectorDDeadline + +==== diff --git a/proof-builder-rpc/src/api/proof_handler.rs b/proof-builder-rpc/src/api/proof_handler.rs index e86b1f4e..d55eb949 100644 --- a/proof-builder-rpc/src/api/proof_handler.rs +++ b/proof-builder-rpc/src/api/proof_handler.rs @@ -67,7 +67,7 @@ pub(super) async fn get_chain_proof_task_desc( block_end: proof.block_end, proof_type: payload.proof_type.to_string(), state: ProofState::from_i64(proof.proof_state) - .unwrap_or_else(|| ProofState::New) + .unwrap_or(ProofState::New) .to_string(), proving_cycles: proof.cycles, proving_time: proof.proving_time, @@ -110,7 +110,7 @@ pub(super) async fn get_operator_proof_task_desc( block_end: operator_proof.execution_layer_block_number + 1, proof_type: "Operator".to_string(), state: ProofState::from_i64(operator_proof.proof_state) - .unwrap_or_else(|| ProofState::New) + .unwrap_or(ProofState::New) .to_string(), proving_cycles: operator_proof.cycles, proving_time: operator_proof.proving_time, diff --git a/proof-builder-rpc/src/config.rs b/proof-builder-rpc/src/config.rs index 7c137c5b..95eb7386 100644 --- a/proof-builder-rpc/src/config.rs +++ b/proof-builder-rpc/src/config.rs @@ -18,7 +18,7 @@ impl ProofBuilderConfig { fn load(url: &str) -> anyhow::Result { let content = - std::fs::read_to_string(&url).context(format!("Failed to read config file: {url}"))?; + std::fs::read_to_string(url).context(format!("Failed to read config file: {url}"))?; Ok(toml::from_str(&content)?) } diff --git a/proof-builder-rpc/src/task/commit_chain_proof.rs b/proof-builder-rpc/src/task/commit_chain_proof.rs index 942da481..5cb4be1a 100644 --- a/proof-builder-rpc/src/task/commit_chain_proof.rs +++ b/proof-builder-rpc/src/task/commit_chain_proof.rs @@ -86,7 +86,7 @@ pub(crate) fn spawn_commit_chain_proof_task( let zkm_version = proof.zkm_version.clone(); let (public_value_hex, proof_size) = builder.save_proof(&ctx, &input, cycles, proof)?; - create_commit_chain_proof(&local_db, block_start, 0xFFffFFff as i64 - block_start, args.output_proof.clone(), public_value_hex, proof_size as i64, cycles, CommitChainProofBuilder::name(), proving_duration as i64, proving_time as i64, store::ProofState::Proven,zkm_version).await?; + create_commit_chain_proof(&local_db, block_start, 0xffffffff_i64 - block_start, args.output_proof.clone(), public_value_hex, proof_size as i64, cycles, CommitChainProofBuilder::name(), proving_duration as i64, proving_time as i64, store::ProofState::Proven,zkm_version).await?; args = ProofBuilderConfig::run_next(args, CommitChainProofBuilder::name())?; } _ = cancellation_token.cancelled() => { diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index a6f795f3..4cc88c0b 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -1,3 +1,5 @@ +#![allow(clippy::too_many_arguments)] // Task persistence APIs mirror the proof metadata schema. + mod commit_chain_proof; mod header_chain_proof; mod operator_proof; @@ -273,14 +275,14 @@ async fn read_watchtower_challenge_details<'a>( task.graph_id ); } - if let Some(first) = challenge_init_txids.first() { - if !challenge_init_txids.iter().all(|x| first == x) { - anyhow::bail!( - "Inconsistent watchtower challenge info from instance {} and graph_id {}", - task.instance_id, - task.graph_id - ); - } + if let Some(first) = challenge_init_txids.first() + && !challenge_init_txids.iter().all(|x| first == x) + { + anyhow::bail!( + "Inconsistent watchtower challenge info from instance {} and graph_id {}", + task.instance_id, + task.graph_id + ); } let challenge_txids: Vec> = watchtower_info .iter() @@ -529,9 +531,9 @@ pub(crate) async fn create_long_running_task( zkm_version: String, ) -> anyhow::Result { let mut storage_processor = local_db.acquire().await?; - Ok(storage_processor + storage_processor .create_long_running_task_proof(&LongRunningTaskProof { - block_start: start as i64, + block_start: start, block_end: start + batch_size, chain_name, path_to_proof: Some(path_to_proof), @@ -546,7 +548,7 @@ pub(crate) async fn create_long_running_task( created_at: current_time_secs(), updated_at: current_time_secs(), }) - .await?) + .await } /// This is a special function to add a new record while updating the previous record's block_end. @@ -567,11 +569,11 @@ pub(crate) async fn create_commit_chain_proof( let mut storage_processor = local_db.start_transaction().await?; // we use start directly since it's block_end is initialized by u64::MAX let previous_proof = storage_processor - .find_long_running_task_proof_including_block_number(start as i64, chain_name.clone()) + .find_long_running_task_proof_including_block_number(start, chain_name.clone()) .await?; tracing::info!("previous_proof: {previous_proof:?}"); if let Some(previous_proof) = previous_proof { - let prev_batch_size = start as i64 - previous_proof.block_start; + let prev_batch_size = start - previous_proof.block_start; tracing::info!("update previous proof from {start} batch_size: {prev_batch_size}"); storage_processor .update_long_running_task_proof_state( @@ -584,7 +586,7 @@ pub(crate) async fn create_commit_chain_proof( } let affected = storage_processor .create_long_running_task_proof(&LongRunningTaskProof { - block_start: start as i64, + block_start: start, block_end: start + batch_size, chain_name, path_to_proof: Some(path_to_proof), @@ -619,7 +621,7 @@ pub(crate) async fn update_long_running_task( ) -> anyhow::Result { let mut storage_processor = local_db.acquire().await?; let task = storage_processor - .find_long_running_task_proof_including_block_number(start_index as i64, chain_name.clone()) + .find_long_running_task_proof_including_block_number(start_index, chain_name.clone()) .await?; if task.is_none() { anyhow::bail!( @@ -627,7 +629,7 @@ pub(crate) async fn update_long_running_task( ); } let total_time_to_proof = (current_time_secs() - task.unwrap().created_at) * 1000; - Ok(storage_processor + storage_processor .update_long_running_task_proof_success( start_index, &chain_name, @@ -641,13 +643,14 @@ pub(crate) async fn update_long_running_task( proving_time, &zkm_version, ) - .await?) + .await } /// table schema: (index, instance_id, graph_id, public_key, challenge_txid, challenge_init_txid, path_to_proof, cycles, state, update_time) /// * index: incremental id /// * state: 0-new, 1-doing, 2-done, 3-failed -/// Invocated by API +/// +/// Invoked by API. pub(crate) async fn add_watchtower_task( local_db: &LocalDB, instance_id: Uuid, @@ -657,7 +660,7 @@ pub(crate) async fn add_watchtower_task( execution_layer_block_number: i64, ) -> anyhow::Result { let mut storage_processor = local_db.acquire().await?; - Ok(storage_processor + storage_processor .create_watchtower_proof(&WatchtowerProof { id: 1, instance_id, @@ -671,7 +674,7 @@ pub(crate) async fn add_watchtower_task( included: true, ..Default::default() }) - .await?) + .await } pub(crate) async fn find_watchtower_task( @@ -719,7 +722,7 @@ pub(crate) async fn update_watchtower_task( zkm_version: String, ) -> anyhow::Result { let mut storage_processor = local_db.acquire().await?; - Ok(storage_processor + storage_processor .update_watchtower_proof( index, path_to_proof, @@ -731,7 +734,7 @@ pub(crate) async fn update_watchtower_task( proving_time, &zkm_version, ) - .await?) + .await } /// table schema: (index, instance_id, graph_id, execution_layer_block_number, path_to_proof, cycles, state, update_time) @@ -819,7 +822,7 @@ pub(crate) async fn add_operator_task( id: 1, instance_id, graph_id, - execution_layer_block_number: execution_layer_block_number as i64, + execution_layer_block_number, proof_state: ProofState::New.to_i64(), created_at: current_time_secs(), updated_at: current_time_secs(), @@ -905,7 +908,7 @@ pub(crate) async fn update_operator_task( zkm_version: String, ) -> anyhow::Result { let mut storage_processor = local_db.acquire().await?; - Ok(storage_processor + storage_processor .update_operator_proof( index, path_to_proof, @@ -917,7 +920,7 @@ pub(crate) async fn update_operator_task( proving_time, &zkm_version, ) - .await?) + .await } #[inline(always)] diff --git a/proof-builder-rpc/src/task/state_chain_proof.rs b/proof-builder-rpc/src/task/state_chain_proof.rs index 5f7302bd..8912c6bb 100644 --- a/proof-builder-rpc/src/task/state_chain_proof.rs +++ b/proof-builder-rpc/src/task/state_chain_proof.rs @@ -82,8 +82,8 @@ async fn spawn_state_chain_ctx_builder( let snap_path = std::path::Path::new(&args.input_proof).parent().unwrap().to_str().unwrap(); tracing::info!("fetch snap_path: {snap_path:?}"); - std::fs::write(&format!("{}/{}.args", snap_path, args.start), serde_json::to_string(&args)?)?; - std::fs::write(&format!("{}/{}.ctx", snap_path, args.start), serde_json::to_string(&ctx)?)?; + std::fs::write(format!("{}/{}.args", snap_path, args.start), serde_json::to_string(&args)?)?; + std::fs::write(format!("{}/{}.ctx", snap_path, args.start), serde_json::to_string(&ctx)?)?; let affected = match create_long_running_task( &local_db, @@ -133,7 +133,7 @@ async fn spawn_state_chain_prover( tracing::info!("prover: start proving, block_start: {start_index}, batch_size: {batch_size}"); let snap_path = std::path::Path::new(&input_proof).parent().unwrap().to_str().unwrap(); - let args: state_chain_proof::Args = match std::fs::read(&format!("{}/{}.args", snap_path, start_index)) { + let args: state_chain_proof::Args = match std::fs::read(format!("{}/{}.args", snap_path, start_index)) { Ok(x) => match serde_json::from_slice(&x) { Ok(args) => args, Err(e) => { @@ -148,7 +148,7 @@ async fn spawn_state_chain_prover( } }; - let ctx: ProofRequest = match &std::fs::read(&format!("{}/{}.ctx", snap_path, start_index)) { + let ctx: ProofRequest = match std::fs::read(format!("{}/{}.ctx", snap_path, start_index)) { Ok(x) => match serde_json::from_slice(&x) { Ok(ctx) => ctx, Err(e) => { @@ -262,18 +262,16 @@ pub(crate) fn spawn_state_chain_proof_task( ) .await?; - if let Some(task_failed) = cur_task_failed { - if let Some(ref task) = cur_task { - if task.block_start == task_failed.block_start - && task.block_end == task_failed.block_end - { - cur_task = Some(task_failed); - } else { - if task.block_start < args.start as i64 { - // load from config - cur_task = None; - } - } + if let Some(task_failed) = cur_task_failed + && let Some(ref task) = cur_task + { + if task.block_start == task_failed.block_start + && task.block_end == task_failed.block_end + { + cur_task = Some(task_failed); + } else if task.block_start < args.start as i64 { + // load from config + cur_task = None; } };