From 3b239d5f4673030a0ec8312ca9b03a187d9bd6ff Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 09:57:17 +0000 Subject: [PATCH 01/12] fix: fetch proofs at specified btc height --- circuits/README.md | 26 +-- circuits/commit-chain-proof/host/src/lib.rs | 4 +- circuits/operator-proof/guest/src/main.rs | 4 +- circuits/operator-proof/host/src/lib.rs | 59 +++--- circuits/operator-proof/host/src/main.rs | 11 +- circuits/proof-builder/src/lib.rs | 8 +- .../bitcoin-light-client-circuit/src/lib.rs | 12 +- ...f6de8c47e145580c980b40691f90d6a7aaa2.json} | 4 +- .../20260118060917_alter_operator_proof.sql | 6 + crates/store/src/localdb.rs | 4 +- crates/store/src/schema.rs | 2 +- .../proof-builder-rpc/proof-builder.toml | 2 +- node/src/action.rs | 64 ++---- node/src/rpc_service/proof.rs | 2 +- node/src/utils.rs | 80 +++++-- proof-builder-rpc/proof-builder.toml.example | 2 +- proof-builder-rpc/src/api/proof_handler.rs | 2 +- proof-builder-rpc/src/api/proofs.rs | 2 +- proof-builder-rpc/src/task/mod.rs | 200 ++++++++++-------- proof-builder-rpc/src/task/operator_proof.rs | 61 +++--- .../src/task/watchtower_proof.rs | 39 ++-- 21 files changed, 313 insertions(+), 281 deletions(-) rename crates/store/.sqlx/{query-7e50ecfde6444e82743d67ea9028d914b36a04991a5aa7ce1d5fe7fb4d3673b3.json => query-128842963b6835643e4a62e8341ff6de8c47e145580c980b40691f90d6a7aaa2.json} (65%) create mode 100644 crates/store/migrations/20260118060917_alter_operator_proof.sql diff --git a/circuits/README.md b/circuits/README.md index 5cb4ec7c..de58d53e 100644 --- a/circuits/README.md +++ b/circuits/README.md @@ -221,23 +221,23 @@ After calling the [`proceedWithdraw`](https://github.com/GOATNetwork/bitvm2-L2-c ``` export BITCOIN_NETWORK=regtest -export GENESIS_SEQUENCER_COMMIT_TXID=$(cat ./data/commit-chain/commit_info.json.0 | jq -r .genesis_txid) +export DIR="/home/ubuntu/data/proof-builder-rpc/circuits" +export GENESIS_SEQUENCER_COMMIT_TXID=$(cat ${DIR}/data/commit-chain/commit_info.json.0 | jq -r .genesis_txid) -export LATEST_SEQUENCER_COMMIT_TXID=$(cat ./data/commit-chain/commit_info.json.2 | jq -r .txid) -export OPERATOR_BLOCKHASH_COMMIT_TXID=$(cat ./data/commit-chain/commit_info.json.2 | jq -r .txid) +export LATEST_SEQUENCER_COMMIT_TXID=$(cat ${DIR}/data/commit-chain/commit_info.json.11 | jq -r .txid) +export OPERATOR_COMMITTED_BLOCKHASH =$(cat ${DIR}/data/commit-chain/commit_info.json.11 | jq -r .txid) -export HEADER_CHAIN_INPUT_PROOF="data/header-chain/0-116000.bin" -export COMMIT_CHAIN_INPUT_PROOF="data/commit-chain/2-1.bin" -export STATE_CHAIN_INPUT_PROOF="data/state-chain/9511050-10.bin" -export LATEST_STATE_BLOCK_HASH="0x7908184bce067fa5a4508d309cbaf22dd1e0b586ad2dd42c0e51a5308a7bd815" +export HEADER_CHAIN_INPUT_PROOF="$DIR/data/header-chain/1024-1.bin" +export COMMIT_CHAIN_INPUT_PROOF="$DIR/data/commit-chain/11-1.bin" +export STATE_CHAIN_INPUT_PROOF="$DIR/data/state-chain/10558271-40.bin" -export GRAPH_ID="0x00112233445566778899aabbccddeeff" -export EXECUTION_LAYER_BLOCK_NUMBER=9511055 +export GRAPH_ID="3C2917B82FE14EF7B8CC8BEF3ECD700F" +export EXECUTION_LAYER_BLOCK_NUMBER=10558309 -export INCLUDED_WATCHTOWERS=1 -export WATCHTOWER_PUBLIC_KEYS="0272efe7ccae21d2541ad85d4f2961f2e5593c29dc8bc37bf87035fc2d5527a651" -export WATCHTOWER_CHALLENGE_TXIDS="3b155884a7f6dd65836045779c6cb5e0ebe11d4630f825fb45682b8cef1c79f0" -export WATCHTOWER_CHALLENGE_INIT_TXID="7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246" +export INCLUDED_WATCHTOWERS=0 +export WATCHTOWER_PUBLIC_KEYS="02e7a08db9093c279535bd0078582469b82bf9f12c6dcb7588e187d2b9cc724279,02f6dce5d37a801064bdf42759dba98afccc80440c32cdc3a8d85c0ed9ae2e749b" +export WATCHTOWER_CHALLENGE_TXIDS="6247824c7c96c4701ef52163316d938412b15cf15962622b3c63f3cf41193f96,fe96c90162c369f45ae4f08140c6197a15c5f3cfe23f04be60f18802e97f4f91" +export WATCHTOWER_CHALLENGE_INIT_TXID="e7723e03ac97172cf033e40d4b9d9c0e22efa7a41eb855a1576f467684a0f6b3" RUST_LOG=info cargo run --package operator-proof --bin operator-proof -r -- --output "data/operator-proof/output.bin" ``` diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 02c1c39a..2de3a7ab 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -94,9 +94,9 @@ pub async fn fetch_commit_chain( for _i in start..start + batch_size { let txid = Txid::from_str(&ci.txid)?; println!("network: {:?}, txid: {txid:?}", btc_client.network()); + let commit_txn = btc_client.get_tx_status(&txid).await?; + let block_height = commit_txn.block_height.unwrap(); let commit_txn = btc_client.get_tx(&txid).await?.unwrap(); - let proof = btc_client.get_merkle_proof_extend(&txid).await?; - let block_height = proof.height as u32; let op_return_data = extract_op_return_data(&commit_txn.output); let mut sequencer_set_hash: [u8; 32] = [0u8; 32]; diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index 91852187..abaa74be 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -28,7 +28,7 @@ pub fn main() { let operator_commit_chain: CommitChainCircuitInput = zkm_zkvm::io::read(); let operator_state_chain: StateChainCircuitInput = zkm_zkvm::io::read(); let spv_ss_commit: SPV = zkm_zkvm::io::read(); - let spv_operator_blockhash: SPV = zkm_zkvm::io::read(); + let operator_committed_blockhash: [u8; 32] = zkm_zkvm::io::read(); let (btc_best_block_hash, constant, included_watchtowers) = bitcoin_light_client_circuit::propose_longest_chain( included_watchertowers, @@ -42,7 +42,7 @@ pub fn main() { operator_commit_chain, operator_state_chain, spv_ss_commit, - spv_operator_blockhash, + operator_committed_blockhash, ); zkm_zkvm::io::commit(&btc_best_block_hash); diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 0bb624cb..4494e07d 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -2,7 +2,7 @@ use alloy_primitives::U256; use anyhow::Context; use bitcoin::{ - Network, ScriptBuf, Transaction, TxOut, Txid, + BlockHash, Network, ScriptBuf, Transaction, TxOut, Txid, hashes::Hash, secp256k1::{PublicKey, XOnlyPublicKey}, }; @@ -15,6 +15,7 @@ use header_chain::{CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevP use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; 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, @@ -43,7 +44,7 @@ pub struct Args { pub latest_sequencer_commit_txid: String, #[clap(long, env)] - pub operator_blockhash_commit_txid: String, + pub operator_committed_blockhash: String, #[clap(long, env)] pub genesis_sequencer_commit_txid: String, @@ -91,7 +92,7 @@ static ELF_ID: OnceLock = OnceLock::new(); pub async fn fetch_target_block_and_watchtower_tx( esplora_url: &str, latest_sequencer_commit_txid: &str, - operator_blockhash_commit_txid: &str, + operator_committed_blockhash: &str, watchtower_challenge_init_txid: &String, watchtower_challenge_txids: &str, watchtower_public_keys: &str, @@ -99,9 +100,7 @@ pub async fn fetch_target_block_and_watchtower_tx( ) -> anyhow::Result<( u32, bitcoin::Block, - u32, - bitcoin::Block, - bitcoin::Transaction, + BlockHash, bitcoin::Transaction, Vec, Vec, @@ -120,13 +119,26 @@ pub async fn fetch_target_block_and_watchtower_tx( tracing::info!("block height: {block_pos_ss_commit}"); let target_block_ss_commit = btc_client.get_block_by_height(block_pos_ss_commit).await.unwrap(); - let operator_blockhash_commit_txid = Txid::from_str(&operator_blockhash_commit_txid).unwrap(); - let operator_blockhash_commit_txn = - btc_client.get_tx(&operator_blockhash_commit_txid).await.unwrap().unwrap(); - let tx_status = btc_client.get_tx_status(&operator_blockhash_commit_txid).await.unwrap(); - let block_pos_operator_blockhash = tx_status.block_height.unwrap(); + let operator_committed_blockhash = BlockHash::from_str(operator_committed_blockhash).unwrap(); let target_block_operator_blockhash = - btc_client.get_block_by_height(block_pos_operator_blockhash).await.unwrap(); + btc_client.get_block_by_hash(&operator_committed_blockhash).await?.unwrap(); + let block_pos_operator_committed_blockhash = + target_block_operator_blockhash.bip34_block_height()? as u32; + + // estimate if the target blocks have been proved. + let tip_block = btc_client.get_height().await?; + let delay_blocks = get_btc_block_confirms(btc_client.network()); + if block_pos_ss_commit + delay_blocks >= tip_block + || block_pos_operator_committed_blockhash + delay_blocks >= tip_block + { + anyhow::bail!( + "Target block is not confirmed enough, tip: {}, ss commit block: {}, operator blockhash commit block: {}, delay_blocks: {}", + tip_block, + block_pos_ss_commit, + block_pos_operator_committed_blockhash, + delay_blocks + ); + } // --- watchtower_challenge_txns --- // let mut watchtower_challenge_txns = Vec::new(); @@ -167,10 +179,8 @@ pub async fn fetch_target_block_and_watchtower_tx( Ok(( block_pos_ss_commit, target_block_ss_commit, - block_pos_operator_blockhash, - target_block_operator_blockhash, + operator_committed_blockhash, operator_latest_sequencer_commit_txn, - operator_blockhash_commit_txn, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, @@ -226,9 +236,7 @@ impl ProofBuilder for OperatorProofBuilder { block_pos_ss_commit, operator_latest_sequencer_commit_txn, - block_pos_operator_blockhash, - target_block_operator_blockhash, - operator_blockhash_commit_txn, + operator_committed_blockhash, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, @@ -331,17 +339,6 @@ impl ProofBuilder for OperatorProofBuilder { &bitcoin_block_headers, ); - tracing::info!( - "construct spv for operator blockhash commit, {}", - operator_blockhash_commit_txn.compute_txid() - ); - let spv_operator_blockhash = build_spv( - &operator_blockhash_commit_txn, - *block_pos_operator_blockhash, - target_block_operator_blockhash.clone(), - &bitcoin_block_headers, - ); - // Generate the proofs let (proof, cycles, proving_time) = tracing::info_span!("generate proof").in_scope( || -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { @@ -364,7 +361,7 @@ impl ProofBuilder for OperatorProofBuilder { stdin.write(&commit_chain_input); stdin.write(&state_chain_input); stdin.write(&spv_ss_commit); - stdin.write(&spv_operator_blockhash); + stdin.write(&operator_committed_blockhash.to_byte_array()); let elf_id = if ELF_ID.get().is_none() { ELF_ID @@ -422,7 +419,7 @@ mod tests { #[tokio::test] #[ignore = "local test"] async fn test_parse_operator_proof() { - let proof_path = "/home/ubuntu/data/proof-builder-rpc/circuits/data/operator/366fb3e0ed2442d39e2cb1e6dda1b08b.bin"; + let proof_path = "/home/ubuntu/data/proof-builder-rpc/circuits/data/operator/3c2917b82fe14ef7b8cc8bef3ecd700f.bin"; let proof_bytes = std::fs::read(proof_path).unwrap(); let vk_bytes = fs::read(format!("{proof_path}.vk_hash.bin")).unwrap(); diff --git a/circuits/operator-proof/host/src/main.rs b/circuits/operator-proof/host/src/main.rs index 84e3d686..145bca9f 100644 --- a/circuits/operator-proof/host/src/main.rs +++ b/circuits/operator-proof/host/src/main.rs @@ -1,5 +1,4 @@ //! Generate operator proof -use bitcoin::constants::TARGET_BLOCK_SPACING; use clap::Parser; use operator_proof::{Args, OperatorProofBuilder, fetch_target_block_and_watchtower_tx}; use proof_builder::{ProofBuilder, ProofRequest}; @@ -15,10 +14,8 @@ async fn main() { let ( block_pos_ss_commit, target_block_ss_commit, - block_pos_operator_blockhash, - target_block_operator_blockhash, + operator_committed_blockhash, operator_latest_sequencer_commit_txn, - operator_blockhash_commit_txn, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, @@ -26,7 +23,7 @@ async fn main() { ) = fetch_target_block_and_watchtower_tx( &args.esplora_url, &args.latest_sequencer_commit_txid, - &args.operator_blockhash_commit_txid, + &args.operator_committed_blockhash, &args.watchtower_challenge_init_txid, &args.watchtower_challenge_txids, &args.watchtower_public_keys, @@ -52,9 +49,7 @@ async fn main() { block_pos_ss_commit, target_block_ss_commit, operator_latest_sequencer_commit_txn, - block_pos_operator_blockhash, - target_block_operator_blockhash, - operator_blockhash_commit_txn, + operator_committed_blockhash, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, diff --git a/circuits/proof-builder/src/lib.rs b/circuits/proof-builder/src/lib.rs index 7dc2877d..b7ad50fa 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use bitcoin::{Block, ScriptBuf, Transaction, TxOut}; +use bitcoin::{Block, BlockHash, ScriptBuf, Transaction, TxOut}; use commit_chain::CircuitCommit; use header_chain::CircuitBlockHeader; use serde::{Deserialize, Serialize}; @@ -59,9 +59,7 @@ pub enum ProofRequest { block_pos_ss_commit: u32, operator_latest_sequencer_commit_txn: Transaction, - target_block_operator_blockhash: Block, - block_pos_operator_blockhash: u32, - operator_blockhash_commit_txn: Transaction, + operator_committed_blockhash: BlockHash, watchtower_challenge_txns: Vec, watchtower_challenge_txn_prev_outs: Vec, @@ -118,5 +116,5 @@ pub struct OnDemandTask { pub included_watchtowers: Vec, pub watchtower_public_keys: Vec, pub graph_id: Option, - pub operator_blockhash_commit_txid: Option, + pub operator_committed_blockhash: Option, } diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index 0b6cc5f2..bb459bcf 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -150,7 +150,7 @@ pub fn propose_longest_chain( commit_chain: CommitChainCircuitInput, state_chain: StateChainCircuitInput, spv_ss_commit: SPV, - spv_operator_blockhash: SPV, + operator_committed_blockhash: [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 @@ -194,8 +194,6 @@ pub fn propose_longest_chain( // verify that the latest_sequecner_commit_tx is in the header chain assert!(spv_ss_commit.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); - // verify that the operator_blockhash is in the header chain - assert!(spv_operator_blockhash.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); @@ -343,10 +341,14 @@ pub fn propose_longest_chain( //let operator_public_input = // hash_operator_inputs(btc_best_block_hash, constant, included_watchtowers); //println!("operator public input hex: {:?}", hex::encode(operator_public_input)); + let included = operator_header_chain + .block_headers + .iter() + .position(|header| header.compute_block_hash() == operator_committed_blockhash); + assert!(included.is_some(), "operator committed blockhash is not included in header chain"); //operator_public_input - let operator_blockhash = spv_operator_blockhash.transaction.0.compute_txid().to_byte_array(); - (operator_blockhash, constant, included_watchtowers.to_le_bytes::<32>()) + (operator_committed_blockhash, constant, included_watchtowers.to_le_bytes::<32>()) } pub fn hash_operator_constant( diff --git a/crates/store/.sqlx/query-7e50ecfde6444e82743d67ea9028d914b36a04991a5aa7ce1d5fe7fb4d3673b3.json b/crates/store/.sqlx/query-128842963b6835643e4a62e8341ff6de8c47e145580c980b40691f90d6a7aaa2.json similarity index 65% rename from crates/store/.sqlx/query-7e50ecfde6444e82743d67ea9028d914b36a04991a5aa7ce1d5fe7fb4d3673b3.json rename to crates/store/.sqlx/query-128842963b6835643e4a62e8341ff6de8c47e145580c980b40691f90d6a7aaa2.json index f70b36fd..4b335a7b 100644 --- a/crates/store/.sqlx/query-7e50ecfde6444e82743d67ea9028d914b36a04991a5aa7ce1d5fe7fb4d3673b3.json +++ b/crates/store/.sqlx/query-128842963b6835643e4a62e8341ff6de8c47e145580c980b40691f90d6a7aaa2.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "INSERT\n INTO operator_proof (instance_id, graph_id, execution_layer_block_number, path_to_proof, public_value_hex, proof_size, cycles, proof_state, total_time_to_proof, proving_time,\n zkm_version, extra, updated_at, created_at, blockhash_commit_txid)\n VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "query": "INSERT\n INTO operator_proof (instance_id, graph_id, execution_layer_block_number, path_to_proof, public_value_hex, proof_size, cycles, proof_state, total_time_to_proof, proving_time,\n zkm_version, extra, updated_at, created_at, operator_committed_blockhash)\n VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", "describe": { "columns": [], "parameters": { @@ -8,5 +8,5 @@ }, "nullable": [] }, - "hash": "7e50ecfde6444e82743d67ea9028d914b36a04991a5aa7ce1d5fe7fb4d3673b3" + "hash": "128842963b6835643e4a62e8341ff6de8c47e145580c980b40691f90d6a7aaa2" } diff --git a/crates/store/migrations/20260118060917_alter_operator_proof.sql b/crates/store/migrations/20260118060917_alter_operator_proof.sql new file mode 100644 index 00000000..c13907b0 --- /dev/null +++ b/crates/store/migrations/20260118060917_alter_operator_proof.sql @@ -0,0 +1,6 @@ +-- Add migration script here +ALTER TABLE `operator_proof` + DROP COLUMN `blockhash_commit_txid`; + +ALTER TABLE `operator_proof` + ADD COLUMN `operator_committed_blockhash` TEXT NOT NULL DEFAULT 'dac7516877b069dac6d2b0430e8b23812392665ecbb0c36c78c8acd12ddc929e'; \ No newline at end of file diff --git a/crates/store/src/localdb.rs b/crates/store/src/localdb.rs index 34dda93b..7d505db5 100644 --- a/crates/store/src/localdb.rs +++ b/crates/store/src/localdb.rs @@ -2584,7 +2584,7 @@ impl<'a> StorageProcessor<'a> { let res = sqlx::query!( "INSERT INTO operator_proof (instance_id, graph_id, execution_layer_block_number, path_to_proof, public_value_hex, proof_size, cycles, proof_state, total_time_to_proof, proving_time, - zkm_version, extra, updated_at, created_at, blockhash_commit_txid) + zkm_version, extra, updated_at, created_at, operator_committed_blockhash) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", operator_proof.instance_id, operator_proof.graph_id, @@ -2600,7 +2600,7 @@ impl<'a> StorageProcessor<'a> { operator_proof.extra, operator_proof.updated_at, operator_proof.created_at, - operator_proof.blockhash_commit_txid, + operator_proof.operator_committed_blockhash, ) .execute(self.conn()) .await?; diff --git a/crates/store/src/schema.rs b/crates/store/src/schema.rs index fcc0635b..66dfb50d 100644 --- a/crates/store/src/schema.rs +++ b/crates/store/src/schema.rs @@ -719,7 +719,7 @@ pub struct OperatorProof { pub id: i64, pub instance_id: Uuid, pub graph_id: Uuid, - pub blockhash_commit_txid: SerializableTxid, + pub operator_committed_blockhash: String, pub execution_layer_block_number: i64, pub path_to_proof: Option, pub public_value_hex: Option, diff --git a/deployment/regtest/proof-builder-rpc/proof-builder.toml b/deployment/regtest/proof-builder-rpc/proof-builder.toml index aabee44c..8e2f637a 100644 --- a/deployment/regtest/proof-builder-rpc/proof-builder.toml +++ b/deployment/regtest/proof-builder-rpc/proof-builder.toml @@ -59,7 +59,7 @@ enable = true esplora_url = "http://127.0.0.1:13002" genesis_sequencer_commit_txid = "5d5a29e1c426abc4b947f90b93c6b043683aafad79cb3b9a9829dfd5e51cad09" latest_sequencer_commit_txid = "dac7516877b069dac6d2b0430e8b23812392665ecbb0c36c78c8acd12ddc929e" -operator_blockhash_commit_txid = "dac7516877b069dac6d2b0430e8b23812392665ecbb0c36c78c8acd12ddc929e" +operator_committed_blockhash = "dac7516877b069dac6d2b0430e8b23812392665ecbb0c36c78c8acd12ddc929e" watchtower_challenge_init_txid = "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246" watchtower_challenge_txids = "3b155884a7f6dd65836045779c6cb5e0ebe11d4630f825fb45682b8cef1c79f0" watchtower_public_keys = "0272efe7ccae21d2541ad85d4f2961f2e5593c29dc8bc37bf87035fc2d5527a651" diff --git a/node/src/action.rs b/node/src/action.rs index 942ca26c..7afbf733 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -11,7 +11,7 @@ use crate::rpc_service::current_time_secs; use crate::utils::*; use alloy::primitives::Address as EvmAddress; use anyhow::{Result, anyhow, bail}; -use bitcoin::hashes::Hash; +use bitcoin::{BlockHash, hashes::Hash}; use bitcoin::{OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm2_lib::actors::Actor; @@ -2771,56 +2771,26 @@ pub async fn recv_and_dispatch( None => return Ok(()), }; let mut graph = Bitvm2Graph::from_simplified(&graph)?; - let watchtower_challenge_init_txid = - graph.watchtower_challenge_init.tx().compute_txid(); // 1. check that all WatchtowerChallenge Connectors are spent - let mut largest_watchtower_challenge_block_height = 0u32; - let mut largest_watchtower_challenge_block_hash = [0u8; 32]; - for watchtower_index in 0..graph.parameters.watchtower_pubkeys.len() { - let watchtower_challenge_vout = 2 * watchtower_index as u32; - match outpoint_spent_txid( - btc_client, - &watchtower_challenge_init_txid, - watchtower_challenge_vout as u64, - ) - .await - { - Ok(Some(txid)) => { - let tx_status = btc_client.get_tx_status(&txid).await?; - if let Some(block_height) = tx_status.block_height { - if block_height > largest_watchtower_challenge_block_height { - largest_watchtower_challenge_block_height = block_height; - largest_watchtower_challenge_block_hash = - tx_status.block_hash.unwrap().to_byte_array(); - } - } else { - tracing::warn!( - "Retry OperatorCommitBlockHashReady for {instance_id}:{graph_id}:{watchtower_index} later: watchtower challenge tx {txid} not confirmed yet" - ); - push_local_unhandled_messages( - local_db, - graph_id, - &message, - todo_funcs::avg_block_time_secs(btc_client.network()) as usize, - ) - .await?; - return Ok(()); - } - } - Ok(None) => { - tracing::warn!( - "Ignore OperatorCommitBlockHashReady for {instance_id}:{graph_id}:{watchtower_index}: watchtower challenge connector not spent yet" - ); - return Ok(()); - } + let largest_watchtower_challenge_block_hash = + match get_largest_watchtower_challenge_block(&graph, btc_client).await { + Ok(d) => d, Err(e) => { tracing::warn!( - "Ignore OperatorCommitBlockHashReady for {instance_id}:{graph_id}: watchtower challenge connector {watchtower_index} not spent yet, error: {e:?}" - ); + "Ignore OperatorCommitBlockHashReady for {instance_id}:{graph_id}: failed to get + largest watchtower challenge block, error: {e:?}" + ); + push_local_unhandled_messages( + local_db, + graph_id, + &message, + todo_funcs::avg_block_time_secs(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(); @@ -2832,7 +2802,7 @@ pub async fn recv_and_dispatch( operator_sign_blockhash_commit( operator_graph_keypair, &mut graph, - &largest_watchtower_challenge_block_hash, + &BlockHash::from(largest_watchtower_challenge_block_hash).to_byte_array(), blockhash_wots_secret_key, )?; build_sign_and_broadcast_tx( diff --git a/node/src/rpc_service/proof.rs b/node/src/rpc_service/proof.rs index 519a0a09..0707ce94 100644 --- a/node/src/rpc_service/proof.rs +++ b/node/src/rpc_service/proof.rs @@ -53,7 +53,7 @@ pub struct ProofDescResponse { pub struct OperatorProofRequest { pub instance_id: String, pub graph_id: String, - pub blockhash_commit_txid: String, + pub operator_committed_blockhash: String, pub execution_layer_block_number: i64, pub watchtower_challenge_txids: Vec, pub included_watchtowers: Vec, diff --git a/node/src/utils.rs b/node/src/utils.rs index 213f09b4..6ecf1388 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -15,8 +15,9 @@ use bitcoin::consensus::encode::{deserialize, serialize}; use bitcoin::hashes::Hash; use bitcoin::key::Keypair; use bitcoin::{ - Address, Amount, CompressedPublicKey, EcdsaSighashType, Network, OutPoint, PrivateKey, - PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, XOnlyPublicKey, + Address, Amount, BlockHash, CompressedPublicKey, EcdsaSighashType, Network, OutPoint, + PrivateKey, PublicKey, ScriptBuf, Sequence, Transaction, TxIn, TxOut, Txid, Witness, + XOnlyPublicKey, }; use bitcoin_light_client_circuit::{VK_HASH_SIZE, build_watchtower_commitment}; use bitvm::treepp::*; @@ -1812,7 +1813,7 @@ pub async fn get_operator_proof( btc_client: &BTCClient, instance_id: Uuid, graph_id: Uuid, - blockhash_commit_txid: SerializableTxid, + 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? { @@ -1851,7 +1852,7 @@ pub async fn get_operator_proof( &OperatorProofRequest { instance_id: instance_id.to_string(), graph_id: graph_id.to_string(), - blockhash_commit_txid: blockhash_commit_txid.0.to_string(), + operator_committed_blockhash, execution_layer_block_number: graph.proceed_withdraw_height, watchtower_challenge_txids, included_watchtowers, @@ -2795,24 +2796,8 @@ pub async fn operator_send_assert_commit( inputs } else { let wots_secret_keys = operator_master_key.wots_keypair_for_graph(graph_id).0; - let connector_g_vout = graph.parameters.watchtower_pubkeys.len() * 2; - let blockhash_commit_txid = match outpoint_spent_txid( - btc_client, - &graph.watchtower_challenge_init.tx().compute_txid(), - connector_g_vout as u64, - ) - .await - { - Ok(Some(txid)) => txid, - Ok(None) => { - bail!( - "challenge-init connector G not spent yet, cannot generate assert-commit proof" - ); - } - Err(e) => { - bail!("failed to check challenge-init connector G spent status: {e}"); - } - }; + 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, @@ -2820,7 +2805,7 @@ pub async fn operator_send_assert_commit( btc_client, instance_id, graph_id, - blockhash_commit_txid.into(), + operator_committed_blockhash.to_string(), ) .await? { @@ -4590,3 +4575,52 @@ mod tests { println!("result: {result:#?}"); } } + +pub async fn get_largest_watchtower_challenge_block( + graph: &Bitvm2Graph, + btc_client: &BTCClient, +) -> anyhow::Result { + let watchtower_challenge_init_txid = graph.watchtower_challenge_init.tx().compute_txid(); + let mut largest_watchtower_challenge_block_height = 0u32; + 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; + match outpoint_spent_txid( + btc_client, + &watchtower_challenge_init_txid, + watchtower_challenge_vout as u64, + ) + .await + { + Ok(Some(txid)) => { + let tx_status = btc_client.get_tx_status(&txid).await?; + if let Some(block_height) = tx_status.block_height { + if block_height > largest_watchtower_challenge_block_height { + largest_watchtower_challenge_block_height = block_height; + largest_watchtower_challenge_block_hash = + tx_status.block_hash.unwrap().clone(); + } + } else { + anyhow::bail!( + "Watchtower challenge tx {txid} for graph {}, index: {watchtower_index} not confirmed yet", + graph.parameters.graph_id + ); + } + } + Ok(None) => { + anyhow::bail!( + "Watchtower challenge connector {watchtower_index} for graph {}, index: {watchtower_index} not spent yet", + graph.parameters.graph_id + ); + } + Err(e) => { + anyhow::bail!( + "Error checking watchtower challenge connector graph: {}, index: {watchtower_index} spent status: {e}", + graph.parameters.graph_id + ); + } + } + } + Ok(largest_watchtower_challenge_block_hash) +} diff --git a/proof-builder-rpc/proof-builder.toml.example b/proof-builder-rpc/proof-builder.toml.example index 6dff27cd..96dce504 100644 --- a/proof-builder-rpc/proof-builder.toml.example +++ b/proof-builder-rpc/proof-builder.toml.example @@ -59,7 +59,7 @@ enable = true esplora_url = "http://127.0.0.1:13002" genesis_sequencer_commit_txid = "5d5a29e1c426abc4b947f90b93c6b043683aafad79cb3b9a9829dfd5e51cad09" latest_sequencer_commit_txid = "dac7516877b069dac6d2b0430e8b23812392665ecbb0c36c78c8acd12ddc929e" -operator_blockhash_commit_txid = "dac7516877b069dac6d2b0430e8b23812392665ecbb0c36c78c8acd12ddc929e" +operator_committed_blockhash = "dac7516877b069dac6d2b0430e8b23812392665ecbb0c36c78c8acd12ddc929e" watchtower_challenge_init_txid = "8e6c8c54cde0404bd7c2928878cdcfb531da7bb8a9935cfb632c8183446cdaf9" watchtower_challenge_txids = "8d3495cc7a0b0627d4e7bc19215e14ea858c97e768b2ce63b2ce2de0d1088db9" watchtower_public_keys = "03e7a08db9093c279535bd0078582469b82bf9f12c6dcb7588e187d2b9cc724279" diff --git a/proof-builder-rpc/src/api/proof_handler.rs b/proof-builder-rpc/src/api/proof_handler.rs index 541915ad..1f75d909 100644 --- a/proof-builder-rpc/src/api/proof_handler.rs +++ b/proof-builder-rpc/src/api/proof_handler.rs @@ -169,7 +169,7 @@ pub(super) async fn post_operator_proof_task( &api_state.local_db, instance_id, graph_id, - payload.blockhash_commit_txid, + payload.operator_committed_blockhash, payload.execution_layer_block_number, payload.watchtower_challenge_txids.clone(), payload.included_watchtowers.clone(), diff --git a/proof-builder-rpc/src/api/proofs.rs b/proof-builder-rpc/src/api/proofs.rs index 31d6bcde..9e811180 100644 --- a/proof-builder-rpc/src/api/proofs.rs +++ b/proof-builder-rpc/src/api/proofs.rs @@ -67,7 +67,7 @@ pub(super) struct ProofDescResponse { pub(super) struct OperatorProofRequest { pub instance_id: String, pub graph_id: String, - pub blockhash_commit_txid: String, + pub operator_committed_blockhash: String, pub execution_layer_block_number: i64, pub watchtower_challenge_txids: Vec, pub included_watchtowers: Vec, diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index 8f6fa3f4..253e6101 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -12,7 +12,7 @@ use crate::task::{ use ::commit_chain_proof::CommitChainProofBuilder; use ::header_chain_proof::HeaderChainProofBuilder; use ::state_chain_proof::StateChainProofBuilder; -use bitcoin::{Network, Txid}; +use bitcoin::{BlockHash, Network, Txid}; use client::btc_chain::BTCClient; use commit_chain::CircuitCommit; use std::str::FromStr; @@ -203,8 +203,6 @@ pub(crate) async fn fetch_latest_long_running_task_by_state( async fn read_watchtower_challenge_details<'a>( storage_processor: &mut store::localdb::StorageProcessor<'a>, is_watchtower: bool, - bitcoin_network: Network, - esplora_url: &str, ) -> anyhow::Result<( i64, i64, @@ -214,7 +212,6 @@ async fn read_watchtower_challenge_details<'a>( Vec, Option, Option, - i64, )> { let ( task_index, @@ -224,7 +221,7 @@ async fn read_watchtower_challenge_details<'a>( included_watchtowers, watchtower_public_keys, graph_id, - operator_blockhash_commit_txid, + operator_committed_blockhash, ) = if is_watchtower { match storage_processor.find_next_watchtower_proof().await? { Some(task) => { @@ -236,7 +233,7 @@ async fn read_watchtower_challenge_details<'a>( task.execution_layer_block_number, None, challenge_txids, - vec![], + vec![true], pubkeys, Some(task.graph_id.as_simple().to_string()), None, @@ -289,47 +286,12 @@ async fn read_watchtower_challenge_details<'a>( included_watchtowers, challenge_public_keys, Some(task.graph_id.as_simple().to_string()), - Some(task.blockhash_commit_txid.0.to_string()), + Some(task.operator_committed_blockhash), ) }; - let btc_block_number = { - let btc_client = BTCClient::new(bitcoin_network, Some(esplora_url)); - let mut block_number = 0; - for challenge_txid in watchtower_challenge_txids.iter() { - // get block number by block hash - let txid = Txid::from_str(challenge_txid).unwrap(); - match btc_client.get_tx_status(&txid).await { - Ok(tx) => { - if let Some(height) = tx.block_height - && tx.confirmed - && (height as i64) > block_number - { - tracing::info!( - "Challenge txid {} is included in block {}, which is before the current block number {}", - challenge_txid, - height, - block_number - ); - block_number = height as i64; - } else { - tracing::warn!("Challenge txid {} is not confirmed yet", challenge_txid); - } - } - Err(e) => { - tracing::warn!( - "Failed to get tx status for txid {}, error: {}", - challenge_txid, - e - ); - } - } - } - block_number - }; - tracing::info!( - "is_watchtower {is_watchtower}, btc_block_number {btc_block_number}, execution_layer_block_number {execution_layer_block_number}" + "is_watchtower {is_watchtower}, operator_committed_blockhash {operator_committed_blockhash:?}, execution_layer_block_number {execution_layer_block_number}" ); Ok(( task_index, @@ -339,8 +301,7 @@ async fn read_watchtower_challenge_details<'a>( included_watchtowers, watchtower_public_keys, graph_id, - operator_blockhash_commit_txid, - btc_block_number, + operator_committed_blockhash, )) } @@ -354,35 +315,6 @@ pub(crate) async fn fetch_on_demand_task( ) -> anyhow::Result> { // btc header chain: always fetch the latest. let mut storage_processor = local_db.acquire().await?; - // commit chain: always fetch the latest - let commit_chain_input_proof = match storage_processor - .find_latest_long_running_task_proof_by_name(CommitChainProofBuilder::name()) - .await? - { - Some(d) => d, - None => { - tracing::error!("Commit chain input proof is not ready"); - return Ok(None); - } - }; - tracing::info!("commit_chain_input_proof: {commit_chain_input_proof:?}"); - let start = commit_chain_input_proof.block_start; - let batch_size = commit_chain_input_proof.block_end - commit_chain_input_proof.block_start; - let commit_chain_input_proof = commit_chain_input_proof.path_to_proof.unwrap(); - let file = std::path::Path::new(&commit_chain_input_proof) - .parent() - .unwrap() - .join(format!("{start}-{batch_size}.bin.commits")); - let content = match std::fs::read_to_string(&file) { - Ok(d) => d, - Err(e) => { - tracing::error!("read {file:?} error, {e}"); - return Ok(None); - } - }; - let commits: Vec = serde_json::from_str(&content)?; - let latest_sequencer_commit_txid = commits[0].commit_txn.compute_txid().to_string(); - let ( task_index, execution_layer_block_number, @@ -391,16 +323,8 @@ pub(crate) async fn fetch_on_demand_task( included_watchtowers, watchtower_public_keys, graph_id, - operator_blockhash_commit_txid, - btc_block_number, - ) = match read_watchtower_challenge_details( - &mut storage_processor, - is_watchtower, - bitcoin_network, - esplora_url, - ) - .await - { + operator_committed_blockhash, + ) = match read_watchtower_challenge_details(&mut storage_processor, is_watchtower).await { Ok(d) => d, Err(e) => { tracing::warn!("Failed to read watchtower challenge details, error: {}", e); @@ -408,19 +332,79 @@ pub(crate) async fn fetch_on_demand_task( } }; - if !is_watchtower && btc_block_number == 0 { + if !is_watchtower && operator_committed_blockhash.is_none() { tracing::warn!("Watchtower challenge tx is not confirmed yet."); return Ok(None); } // If we finish the sequencer set commitment shortly, the latest commit txid is confirmed after `btc_block_number`, which leads to the latest_commit_txid not included in header chain proof. + + let mut largest_btc_block_height = 0; + let btc_client = BTCClient::new(bitcoin_network, Some(esplora_url)); + for txid in &watchtower_challenge_txids { + let txid = Txid::from_str(txid)?; + let block_height = match btc_client.get_tx_status(&txid).await { + Ok(tx_status) => { + if let Some(block_height) = tx_status.block_height { + block_height + } else { + tracing::warn!( + "Challenge tx {txid} is not confirmed yet, wait for the next round" + ); + return Ok(None); + } + } + Err(e) => { + tracing::warn!( + "Challenge tx {txid} is not found in BTC node, it might be mempool tx, wait for the next round, error: {e}" + ); + return Ok(None); + } + }; + if block_height > largest_btc_block_height { + largest_btc_block_height = block_height; + } + } + + // 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" + ); + } + } + Ok(None) => { + tracing::warn!( + "Operator committed block hash {hash} is not found in BTC, it might be mempool tx, wait for the next round" + ); + return Ok(None); + } + Err(e) => { + tracing::warn!( + "Failed to fetch operator committed block height by hash {hash}, error: {e}" + ); + return Ok(None); + } + }, + None => {} // skip for watchtowers + }; + let header_chain_input_proof = match storage_processor - .find_latest_long_running_task_proof_by_name(HeaderChainProofBuilder::name()) + .find_long_running_task_proof_including_block_number( + largest_btc_block_height as i64, + HeaderChainProofBuilder::name(), + ) .await? { Some(d) => d, None => { - tracing::warn!("Header chain input proof is not ready"); + tracing::warn!( + "Header chain proof is not ready for block: {}, proof not ready", + largest_btc_block_height + ); return Ok(None); } }; @@ -454,6 +438,38 @@ pub(crate) async fn fetch_on_demand_task( }; let state_chain_input_proof = state_chain_input_proof.path_to_proof.unwrap(); + // commit chain + let commit_chain_input_proof = match storage_processor + .find_long_running_task_proof_including_block_number( + largest_btc_block_height as i64, + CommitChainProofBuilder::name(), + ) + .await? + { + Some(d) => d, + None => { + tracing::error!("Commit chain input proof is not ready"); + return Ok(None); + } + }; + tracing::info!("commit_chain_input_proof: {commit_chain_input_proof:?}"); + let start = commit_chain_input_proof.block_start; + let batch_size = commit_chain_input_proof.block_end - commit_chain_input_proof.block_start; + let commit_chain_input_proof = commit_chain_input_proof.path_to_proof.unwrap(); + let file = std::path::Path::new(&commit_chain_input_proof) + .parent() + .unwrap() + .join(format!("{start}-{batch_size}.bin.commits")); + let content = match std::fs::read_to_string(&file) { + Ok(d) => d, + Err(e) => { + tracing::error!("read {file:?} error, {e}"); + return Ok(None); + } + }; + let commits: Vec = serde_json::from_str(&content)?; + let latest_sequencer_commit_txid = commits[0].commit_txn.compute_txid().to_string(); + Ok(Some(OnDemandTask { task_index, latest_sequencer_commit_txid, @@ -465,7 +481,7 @@ pub(crate) async fn fetch_on_demand_task( included_watchtowers, watchtower_public_keys, graph_id, - operator_blockhash_commit_txid, + operator_committed_blockhash, })) } @@ -647,7 +663,7 @@ pub(crate) async fn add_operator_task( local_db: &LocalDB, instance_id: Uuid, graph_id: Uuid, - blockhash_commit_txid: String, + operator_committed_blockhash: String, execution_layer_block_number: i64, watchtower_challenge_txids: Vec, included_watchtowers: Vec, @@ -718,7 +734,7 @@ pub(crate) async fn add_operator_task( created_at: current_time_secs(), updated_at: current_time_secs(), cycles: 0, - blockhash_commit_txid: Txid::from_str(&blockhash_commit_txid)?.into(), + operator_committed_blockhash, ..Default::default() }) .await?; @@ -835,7 +851,7 @@ mod tests { "4506cf35cd70b3006fe3ce4a87ca1f9b0a76f348cfb529423e2d4c163c28d604".to_string(), "f16286f143430a229c6d068798cd9ba751e83202de5045cc788aab227114cdb2".to_string(), ]; - let blockhash_commit_txid = + let operator_committed_blockhash = "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246".to_string(); let included_watchtowers = vec![true, false]; @@ -843,7 +859,7 @@ mod tests { &local_db, instance_id, graph_id, - blockhash_commit_txid, + operator_committed_blockhash, number, watchtower_challenge_txids, included_watchtowers, diff --git a/proof-builder-rpc/src/task/operator_proof.rs b/proof-builder-rpc/src/task/operator_proof.rs index 11fa779f..6717b063 100644 --- a/proof-builder-rpc/src/task/operator_proof.rs +++ b/proof-builder-rpc/src/task/operator_proof.rs @@ -35,38 +35,45 @@ pub(crate) fn spawn_operator_proof_task( _ = tokio::time::sleep(Duration::from_secs(interval)) => { // fetch args from the database. let task_index; - if let Some(next_task) = fetch_on_demand_task( + match fetch_on_demand_task( &local_db, false, args.bitcoin_network, &args.esplora_url, - ).await? { - args.latest_sequencer_commit_txid = next_task.latest_sequencer_commit_txid; - args.header_chain_input_proof = next_task.header_chain_input_proof; - args.commit_chain_input_proof = next_task.commit_chain_input_proof; - args.state_chain_input_proof = next_task.state_chain_input_proof; - args.operator_blockhash_commit_txid = next_task.operator_blockhash_commit_txid.unwrap(); - args.graph_id = next_task.graph_id.unwrap(); - args.output = format!("{}/{}.bin", - std::path::Path::new(&args.output).parent().unwrap().to_str().unwrap(), - 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(","); - // 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; - } else { - tokio::time::sleep(Duration::from_secs(5)).await; - continue; + ).await { + Ok(Some(next_task)) => { + args.latest_sequencer_commit_txid = next_task.latest_sequencer_commit_txid; + args.header_chain_input_proof = next_task.header_chain_input_proof; + args.commit_chain_input_proof = next_task.commit_chain_input_proof; + args.state_chain_input_proof = next_task.state_chain_input_proof; + args.operator_committed_blockhash = next_task.operator_committed_blockhash.unwrap(); + args.graph_id = next_task.graph_id.unwrap(); + args.output = format!("{}/{}.bin", + std::path::Path::new(&args.output).parent().unwrap().to_str().unwrap(), + 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(","); + // 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; + } + Ok(None) => { + tracing::warn!("No on demand task found for operator proof, wait for the next round"); + tokio::time::sleep(Duration::from_secs(5)).await; + continue; + } + Err(e) => { + tracing::error!("Failed to fetch on demand task for operator proof, error: {e}"); + tokio::time::sleep(Duration::from_secs(5)).await; + continue; + } }; info!("Operator proof generate task: generate proof, args: {args:?}"); let ( block_pos_ss_commit, target_block_ss_commit, - block_pos_operator_blockhash, - target_block_operator_blockhash, + operator_committed_blockhash, operator_latest_sequencer_commit_txn, - operator_blockhash_commit_txn, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, watchtower_challenge_txn_pubkeys, @@ -74,7 +81,7 @@ pub(crate) fn spawn_operator_proof_task( ) = match fetch_target_block_and_watchtower_tx( &args.esplora_url, &args.latest_sequencer_commit_txid, - &args.operator_blockhash_commit_txid, + &args.operator_committed_blockhash, &args.watchtower_challenge_init_txid, &args.watchtower_challenge_txids, &args.watchtower_public_keys, @@ -104,9 +111,7 @@ pub(crate) fn spawn_operator_proof_task( target_block_ss_commit, operator_latest_sequencer_commit_txn, - block_pos_operator_blockhash, - target_block_operator_blockhash, - operator_blockhash_commit_txn, + operator_committed_blockhash, watchtower_challenge_txns, watchtower_challenge_txn_prev_outs, diff --git a/proof-builder-rpc/src/task/watchtower_proof.rs b/proof-builder-rpc/src/task/watchtower_proof.rs index 06990839..c795ecd8 100644 --- a/proof-builder-rpc/src/task/watchtower_proof.rs +++ b/proof-builder-rpc/src/task/watchtower_proof.rs @@ -31,25 +31,34 @@ pub(crate) fn spawn_watchtower_proof_task( _ = tokio::time::sleep(Duration::from_secs(interval)) => { // fetch args from the database by instance id and graph id. let task_index; - if let Some(next_task) = fetch_on_demand_task( + match fetch_on_demand_task( &local_db, true, args.bitcoin_network, &args.esplora_url, - ).await? { - args.latest_sequencer_commit_txid = next_task.latest_sequencer_commit_txid; - args.header_chain_input_proof = next_task.header_chain_input_proof; - args.commit_chain_input_proof = next_task.commit_chain_input_proof; - args.state_chain_input_proof = next_task.state_chain_input_proof; - args.output = format!("{}/{}.bin", - std::path::Path::new(&args.output).parent().unwrap().to_str().unwrap(), - next_task.task_index, - ); - task_index = next_task.task_index; - } else { - tokio::time::sleep(Duration::from_secs(5)).await; - continue; - }; + ).await { + Ok(Some(next_task)) => { + args.latest_sequencer_commit_txid = next_task.latest_sequencer_commit_txid; + args.header_chain_input_proof = next_task.header_chain_input_proof; + args.commit_chain_input_proof = next_task.commit_chain_input_proof; + args.state_chain_input_proof = next_task.state_chain_input_proof; + args.output = format!("{}/{}.bin", + std::path::Path::new(&args.output).parent().unwrap().to_str().unwrap(), + next_task.task_index, + ); + task_index = next_task.task_index; + }, + Ok(None) => { + tracing::warn!("No on demand task found for watchtower proof, wait for the next round"); + tokio::time::sleep(Duration::from_secs(5)).await; + continue; + } + Err(e) => { + tracing::error!("Failed to fetch on demand task for watchtower proof, error: {e}"); + tokio::time::sleep(Duration::from_secs(5)).await; + continue; + } + } info!("Watchtower proof generate task: generate proof, args: {args:?}"); let (block_pos, target_block, latest_sequencer_commit_tx) = From 65d897bb26bf3e321b5f7eac6c5acf992f4d1946 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 10:07:27 +0000 Subject: [PATCH 02/12] fix: fetch proofs at specified btc height --- proof-builder-rpc/src/task/mod.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index 253e6101..cb0df35b 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -226,13 +226,15 @@ async fn read_watchtower_challenge_details<'a>( match storage_processor.find_next_watchtower_proof().await? { Some(task) => { tracing::info!("watchtower task: {task:?}"); - let challenge_txids: Vec = vec![task.challenge_txid.0.to_string()]; + // 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 pubkeys: Vec = vec![task.public_key.clone()]; ( task.id, task.execution_layer_block_number, None, - challenge_txids, + watchtower_challenge_txids, vec![true], pubkeys, Some(task.graph_id.as_simple().to_string()), From 56d61f117a1f5587eb0554b1797457de8e6fbfa0 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 10:42:37 +0000 Subject: [PATCH 03/12] fix: publish ss with specified evm block hash --- node/src/action.rs | 2 +- .../src/task/commit_chain_proof.rs | 6 +- .../src/task/header_chain_proof.rs | 2 +- proof-builder-rpc/src/task/mod.rs | 68 ++++++++++++++++++- .../src/task/state_chain_proof.rs | 4 +- 5 files changed, 73 insertions(+), 9 deletions(-) diff --git a/node/src/action.rs b/node/src/action.rs index 7afbf733..36015a20 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -2802,7 +2802,7 @@ pub async fn recv_and_dispatch( operator_sign_blockhash_commit( operator_graph_keypair, &mut graph, - &BlockHash::from(largest_watchtower_challenge_block_hash).to_byte_array(), + &largest_watchtower_challenge_block_hash.to_byte_array(), blockhash_wots_secret_key, )?; build_sign_and_broadcast_tx( diff --git a/proof-builder-rpc/src/task/commit_chain_proof.rs b/proof-builder-rpc/src/task/commit_chain_proof.rs index aa052f41..a9c941ee 100644 --- a/proof-builder-rpc/src/task/commit_chain_proof.rs +++ b/proof-builder-rpc/src/task/commit_chain_proof.rs @@ -1,5 +1,5 @@ use crate::ProofBuilderConfig; -use crate::task::create_long_running_task; +use crate::task::create_commit_chain_proof; use crate::task::fetch_latest_long_running_task; use commit_chain_proof::CommitChainProofBuilder; use commit_chain_proof::fetch_commit_chain; @@ -64,6 +64,7 @@ pub(crate) fn spawn_commit_chain_proof_task( continue; } }; + let block_start = commits.first().unwrap().block_height as i64; let ctx = ProofRequest::CommitChainProofRequest { init_input: args.init_input, @@ -83,7 +84,8 @@ pub(crate) fn spawn_commit_chain_proof_task( let proving_duration = proving_start.elapsed().as_secs_f32() * 1000.0; let zkm_version = proof.zkm_version.clone(); let (public_value_hex, proof_size) = builder.save_proof(&ctx, &input, cycles, proof)?; - create_long_running_task(&local_db, args.start as u64, args.batch_size as u64, 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, i64::MAX, 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/header_chain_proof.rs b/proof-builder-rpc/src/task/header_chain_proof.rs index 181a60ff..ee29e9e0 100644 --- a/proof-builder-rpc/src/task/header_chain_proof.rs +++ b/proof-builder-rpc/src/task/header_chain_proof.rs @@ -82,7 +82,7 @@ pub(crate) fn spawn_header_chain_proof_task( let proving_duration = proving_start.elapsed().as_secs_f32() * 1000.0; let zkm_version = proof.zkm_version.clone(); let (public_value_hex, proof_size) = builder.save_proof(&ctx, &input, cycles, proof)?; - create_long_running_task(&local_db, args.start as u64, args.batch_size as u64, args.output_proof.clone(), public_value_hex, proof_size as i64, cycles, HeaderChainProofBuilder::name(), proving_duration as i64, proving_time as i64, store::ProofState::Proven, zkm_version).await?; + create_long_running_task(&local_db, args.start as i64, args.batch_size as i64, args.output_proof.clone(), public_value_hex, proof_size as i64, cycles, HeaderChainProofBuilder::name(), proving_duration as i64, proving_time as i64, store::ProofState::Proven, zkm_version).await?; args = ProofBuilderConfig::run_next(args, HeaderChainProofBuilder::name())?; } _ = cancellation_token.cancelled() => { diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index cb0df35b..1c66009d 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -491,8 +491,8 @@ pub(crate) async fn fetch_on_demand_task( /// * table_name: header-chain | state-chain | commit-chain pub(crate) async fn create_long_running_task( local_db: &LocalDB, - start: u64, - batch_size: u64, + start: i64, + batch_size: i64, path_to_proof: String, public_value_hex: String, proof_size: i64, @@ -507,7 +507,7 @@ pub(crate) async fn create_long_running_task( Ok(storage_processor .create_long_running_task_proof(&LongRunningTaskProof { block_start: start as i64, - block_end: (start + batch_size) as i64, + block_end: start + batch_size, chain_name, path_to_proof: Some(path_to_proof), public_value_hex: Some(public_value_hex), @@ -524,6 +524,63 @@ pub(crate) async fn create_long_running_task( .await?) } +/// This is a special function to add a new record while updating the previous record's block_end. +pub(crate) async fn create_commit_chain_proof( + local_db: &LocalDB, + start: i64, + batch_size: i64, + path_to_proof: String, + public_value_hex: String, + proof_size: i64, + cycles: u64, + chain_name: String, + total_time_to_proof: i64, + proving_time: i64, + proof_state: ProofState, + zkm_version: String, +) -> anyhow::Result { + 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()) + .await?; + if previous_proof.is_none() { + anyhow::bail!("Current proof not found for block: {}, chain_name: {}", start, chain_name); + } + + let previous_proof = previous_proof.unwrap(); + let prev_batch_size = start as i64 - previous_proof.block_start; + storage_processor + .update_long_running_task_proof_state( + previous_proof.block_start, + &previous_proof.chain_name, + prev_batch_size, + previous_proof.proof_state, + ) + .await?; + + let affected = storage_processor + .create_long_running_task_proof(&LongRunningTaskProof { + block_start: start as i64, + block_end: start + batch_size, + chain_name, + path_to_proof: Some(path_to_proof), + public_value_hex: Some(public_value_hex), + proof_size, + cycles: cycles as i64, + proof_state: proof_state.to_i64(), + total_time_to_proof, + proving_time, + zkm_version, + extra: None, + created_at: current_time_secs(), + updated_at: current_time_secs(), + }) + .await?; + storage_processor.commit().await?; + Ok(affected) +} + pub(crate) async fn update_long_running_task( local_db: &LocalDB, start_index: i64, @@ -856,6 +913,9 @@ mod tests { let operator_committed_blockhash = "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246".to_string(); + let watchtower_challenge_init_txid = + "7f7b4344adb1b8937ddb7124e4f8bba80ee9adf5e8119de76ca8736816bda246".to_string(); + let included_watchtowers = vec![true, false]; add_operator_task( &local_db, @@ -865,6 +925,8 @@ mod tests { number, watchtower_challenge_txids, included_watchtowers, + watchtower_challenge_init_txid, + vec![], ) .await .unwrap(); diff --git a/proof-builder-rpc/src/task/state_chain_proof.rs b/proof-builder-rpc/src/task/state_chain_proof.rs index 5ee5a071..a684811a 100644 --- a/proof-builder-rpc/src/task/state_chain_proof.rs +++ b/proof-builder-rpc/src/task/state_chain_proof.rs @@ -87,8 +87,8 @@ async fn spawn_state_chain_ctx_builder( let affected = match create_long_running_task( &local_db, - args.start, - args.batch_size, + args.start as i64, + args.batch_size as i64, args.output_proof.clone(), "".to_string(), 0, From 1db782a2ece515c2c13b7591e97f543eed332304 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 10:51:01 +0000 Subject: [PATCH 04/12] fix: lint --- node/src/action.rs | 2 +- node/src/utils.rs | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/node/src/action.rs b/node/src/action.rs index 36015a20..852f0543 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -11,7 +11,7 @@ use crate::rpc_service::current_time_secs; use crate::utils::*; use alloy::primitives::Address as EvmAddress; use anyhow::{Result, anyhow, bail}; -use bitcoin::{BlockHash, hashes::Hash}; +use bitcoin::hashes::Hash; use bitcoin::{OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm2_lib::actors::Actor; diff --git a/node/src/utils.rs b/node/src/utils.rs index 6ecf1388..29860041 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -4598,8 +4598,7 @@ pub async fn get_largest_watchtower_challenge_block( if let Some(block_height) = tx_status.block_height { if block_height > largest_watchtower_challenge_block_height { largest_watchtower_challenge_block_height = block_height; - largest_watchtower_challenge_block_hash = - tx_status.block_hash.unwrap().clone(); + largest_watchtower_challenge_block_hash = tx_status.block_hash.unwrap(); } } else { anyhow::bail!( From c0cf91cebaf2f0e9c16d2a8a7979e2b9e12c8b32 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 12:14:10 +0000 Subject: [PATCH 05/12] fix: lint --- circuits/commit-chain-proof/host/src/lib.rs | 73 +++++++++---------- circuits/commit-chain-proof/host/src/main.rs | 2 - .../src/task/commit_chain_proof.rs | 9 +-- proof-builder-rpc/src/task/mod.rs | 24 +++--- 4 files changed, 49 insertions(+), 59 deletions(-) diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 2de3a7ab..9095796c 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -79,51 +79,48 @@ pub async fn fetch_commit_chain( esplora_url: &str, commit_info_file: &str, commits_file: &str, - start: usize, - batch_size: usize, network: Network, ) -> anyhow::Result> { let btc_client = BTCClient::new(network, Some(&esplora_url)); - let rdr = std::fs::File::open(commit_info_file).context("read error")?; + 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 ci: CommitInfo = serde_json::from_reader(rdr)?; - // NOTE: we support one commit-info per commit file currently - assert_eq!(batch_size, 1); - let mut commits: Vec = vec![]; - for _i in start..start + batch_size { - let txid = Txid::from_str(&ci.txid)?; - println!("network: {:?}, txid: {txid:?}", btc_client.network()); - let commit_txn = btc_client.get_tx_status(&txid).await?; - let block_height = commit_txn.block_height.unwrap(); - let commit_txn = btc_client.get_tx(&txid).await?.unwrap(); - - let op_return_data = extract_op_return_data(&commit_txn.output); - let mut sequencer_set_hash: [u8; 32] = [0u8; 32]; - sequencer_set_hash.copy_from_slice(&op_return_data[0..32]); - - if let tendermint::Hash::Sha256(expected_hash) = sequencer_hash(&ci.sequencers) { - assert_eq!(expected_hash, sequencer_set_hash); - } else { - panic!("Invalid sequencer set hash"); - } - - let publisher_public_keys = ci - .publisher_public_keys - .iter() - .map(|compressed_pk| PublicKey::from_str(compressed_pk).unwrap()) - .collect(); - tracing::info!("sequencer_hash: {:?}", sequencer_hash(&ci.sequencers)); - let commit = CircuitCommit { - commit_txn, - sequencers: ci.sequencers.clone(), - publisher_public_keys, - threshold: ci.threshold, - genesis_txid: Txid::from_str(&ci.genesis_txid)?.as_raw_hash().to_byte_array(), - block_height, - }; - commits.push(commit); + let txid = Txid::from_str(&ci.txid)?; + let commit_txn = btc_client.get_tx_status(&txid).await?; + let block_height = commit_txn.block_height.unwrap(); + let commit_txn = btc_client.get_tx(&txid).await?.unwrap(); + + let op_return_data = extract_op_return_data(&commit_txn.output); + let mut sequencer_set_hash: [u8; 32] = [0u8; 32]; + sequencer_set_hash.copy_from_slice(&op_return_data[0..32]); + + if let tendermint::Hash::Sha256(expected_hash) = sequencer_hash(&ci.sequencers) { + assert_eq!(expected_hash, sequencer_set_hash); + } else { + panic!("Invalid sequencer set hash"); } + + let publisher_public_keys = ci + .publisher_public_keys + .iter() + .map(|compressed_pk| PublicKey::from_str(compressed_pk).unwrap()) + .collect(); + tracing::info!("sequencer_hash: {:?}", sequencer_hash(&ci.sequencers)); + let commit = CircuitCommit { + commit_txn, + sequencers: ci.sequencers.clone(), + publisher_public_keys, + threshold: ci.threshold, + genesis_txid: Txid::from_str(&ci.genesis_txid)?.as_raw_hash().to_byte_array(), + block_height, + }; + commits.push(commit); std::fs::write(&commits_file, serde_json::to_vec(&commits)?) .expect(&format!("write {commits_file} error")); Ok(commits) diff --git a/circuits/commit-chain-proof/host/src/main.rs b/circuits/commit-chain-proof/host/src/main.rs index 04c3b5d2..c6fd0344 100644 --- a/circuits/commit-chain-proof/host/src/main.rs +++ b/circuits/commit-chain-proof/host/src/main.rs @@ -15,8 +15,6 @@ async fn main() { &args.esplora_url, &args.commit_info, &args.commits, - args.start, - args.batch_size, args.bitcoin_network, ) .await diff --git a/proof-builder-rpc/src/task/commit_chain_proof.rs b/proof-builder-rpc/src/task/commit_chain_proof.rs index a9c941ee..97bb435b 100644 --- a/proof-builder-rpc/src/task/commit_chain_proof.rs +++ b/proof-builder-rpc/src/task/commit_chain_proof.rs @@ -37,9 +37,8 @@ pub(crate) fn spawn_commit_chain_proof_task( args.start = next_task.block_end as usize; args.input_proof = next_task.path_to_proof.unwrap(); args.commit_info = format!( - "{}/commit_info.json.{}", + "{}/commit_info.json.latest", std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), - args.start, ); args.output_proof = format!( "{}/{}-{}.bin", @@ -57,10 +56,10 @@ pub(crate) fn spawn_commit_chain_proof_task( } info!("Commit chain proof generate task: generate proof, args: {args:?}"); - let commits = match fetch_commit_chain(&args.esplora_url, &args.commit_info, &args.commits, args.start, args.batch_size, args.bitcoin_network).await { + let commits = match fetch_commit_chain(&args.esplora_url, &args.commit_info, &args.commits, args.bitcoin_network).await { Ok(d) => d, Err(err) => { - tracing::info!("Fetch commit chain error, {err:?}, continuing"); + tracing::warn!("Fetch commit chain error, {err:?}, continuing"); continue; } }; @@ -85,7 +84,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, i64::MAX, 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, args.batch_size as i64, 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 1c66009d..2cf52524 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -544,21 +544,17 @@ pub(crate) async fn create_commit_chain_proof( let previous_proof = storage_processor .find_long_running_task_proof_including_block_number(start as i64, chain_name.clone()) .await?; - if previous_proof.is_none() { - anyhow::bail!("Current proof not found for block: {}, chain_name: {}", start, chain_name); + if let Some(previous_proof) = previous_proof { + let prev_batch_size = start as i64 - previous_proof.block_start; + storage_processor + .update_long_running_task_proof_state( + previous_proof.block_start, + &previous_proof.chain_name, + prev_batch_size, + previous_proof.proof_state, + ) + .await?; } - - let previous_proof = previous_proof.unwrap(); - let prev_batch_size = start as i64 - previous_proof.block_start; - storage_processor - .update_long_running_task_proof_state( - previous_proof.block_start, - &previous_proof.chain_name, - prev_batch_size, - previous_proof.proof_state, - ) - .await?; - let affected = storage_processor .create_long_running_task_proof(&LongRunningTaskProof { block_start: start as i64, From 79e00f467595eb048c9b94c9823350ccf8c20447 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 13:36:42 +0000 Subject: [PATCH 06/12] fix: commit chain --- crates/store/src/localdb.rs | 16 ++++++++++++++++ proof-builder-rpc/src/task/commit_chain_proof.rs | 8 +++++--- proof-builder-rpc/src/task/mod.rs | 10 ++++++++++ 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/store/src/localdb.rs b/crates/store/src/localdb.rs index 7d505db5..068e95d0 100644 --- a/crates/store/src/localdb.rs +++ b/crates/store/src/localdb.rs @@ -2490,6 +2490,22 @@ impl<'a> StorageProcessor<'a> { Ok(res.rows_affected()) } + pub async fn find_all_running_task_proofs_by_name( + &mut self, + chain_name: String, + ) -> anyhow::Result> { + let res = sqlx::query_as!( + LongRunningTaskProof, + "SELECT block_start, block_end, chain_name, path_to_proof, public_value_hex, proof_size, cycles, proof_state, total_time_to_proof, proving_time, + zkm_version, extra, updated_at, created_at FROM long_running_task_proof + WHERE chain_name = ?", + chain_name, + ) + .fetch_all(self.conn()) + .await?; + Ok(res) + } + pub async fn find_long_running_task_proof_including_block_number( &mut self, block_number: i64, diff --git a/proof-builder-rpc/src/task/commit_chain_proof.rs b/proof-builder-rpc/src/task/commit_chain_proof.rs index 97bb435b..942da481 100644 --- a/proof-builder-rpc/src/task/commit_chain_proof.rs +++ b/proof-builder-rpc/src/task/commit_chain_proof.rs @@ -1,6 +1,7 @@ use crate::ProofBuilderConfig; use crate::task::create_commit_chain_proof; use crate::task::fetch_latest_long_running_task; +use crate::task::fetch_next_commit_task_index; use commit_chain_proof::CommitChainProofBuilder; use commit_chain_proof::fetch_commit_chain; use proof_builder::{ProofBuilder, ProofRequest}; @@ -34,11 +35,12 @@ pub(crate) fn spawn_commit_chain_proof_task( let next_task = fetch_latest_long_running_task(&local_db, CommitChainProofBuilder::name()).await?; if let Some(next_task) = next_task { info!("Commit chain's next task: {next_task:?}"); - args.start = next_task.block_end as usize; + args.start = fetch_next_commit_task_index(&local_db).await?; args.input_proof = next_task.path_to_proof.unwrap(); args.commit_info = format!( - "{}/commit_info.json.latest", + "{}/commit_info.json.{}", std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), + args.start, ); args.output_proof = format!( "{}/{}-{}.bin", @@ -84,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, args.batch_size as i64, 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 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?; 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 2cf52524..5e867a94 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -181,6 +181,14 @@ pub(crate) async fn run_generate_proof_tasks( Ok("tasks_completed".to_string()) } +pub(crate) async fn fetch_next_commit_task_index(local_db: &LocalDB) -> anyhow::Result { + let mut storage_processor = local_db.acquire().await?; + let index = storage_processor + .find_all_running_task_proofs_by_name(CommitChainProofBuilder::name()) + .await?; + Ok(index.len()) +} + pub(crate) async fn fetch_latest_long_running_task( local_db: &LocalDB, chain_name: String, @@ -544,8 +552,10 @@ pub(crate) async fn create_commit_chain_proof( let previous_proof = storage_processor .find_long_running_task_proof_including_block_number(start as i64, 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; + tracing::info!("update previous proof from {start} batch_size: {prev_batch_size}"); storage_processor .update_long_running_task_proof_state( previous_proof.block_start, From e5d4b9c88eddb036d9098e89df3749563e81484f Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 14:36:08 +0000 Subject: [PATCH 07/12] fix: check blockhash in circuit by spv --- proof-builder-rpc/src/task/mod.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/proof-builder-rpc/src/task/mod.rs b/proof-builder-rpc/src/task/mod.rs index 5e867a94..7afea507 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -463,13 +463,8 @@ pub(crate) async fn fetch_on_demand_task( } }; tracing::info!("commit_chain_input_proof: {commit_chain_input_proof:?}"); - let start = commit_chain_input_proof.block_start; - let batch_size = commit_chain_input_proof.block_end - commit_chain_input_proof.block_start; let commit_chain_input_proof = commit_chain_input_proof.path_to_proof.unwrap(); - let file = std::path::Path::new(&commit_chain_input_proof) - .parent() - .unwrap() - .join(format!("{start}-{batch_size}.bin.commits")); + let file = format!("{commit_chain_input_proof}.commits"); let content = match std::fs::read_to_string(&file) { Ok(d) => d, Err(e) => { From a6c23ff5d9bd32c8870133bd72a8865a0522aec8 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 16:12:58 +0000 Subject: [PATCH 08/12] fix: constrain header chain size to 1 --- .../bitcoin-light-client-circuit/src/lib.rs | 17 ++++---- node/README.md | 42 +++++++++++++------ .../src/task/header_chain_proof.rs | 5 +++ 3 files changed, 44 insertions(+), 20 deletions(-) diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index bb459bcf..9693310a 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -338,14 +338,15 @@ pub fn propose_longest_chain( println!("btc_best_block_hash hex: {:?}", hex::encode(btc_best_block_hash)); println!("included_watchtowers: {:?}", hex::encode(included_watchtowers.to_le_bytes::<32>())); - //let operator_public_input = - // hash_operator_inputs(btc_best_block_hash, constant, included_watchtowers); - //println!("operator public input hex: {:?}", hex::encode(operator_public_input)); - let included = operator_header_chain - .block_headers - .iter() - .position(|header| header.compute_block_hash() == operator_committed_blockhash); - assert!(included.is_some(), "operator committed blockhash is not included in header chain"); + //let included = operator_header_chain + // .block_headers + // .iter() + // .position(|header| header.compute_block_hash() == operator_committed_blockhash); + //assert!(included.is_some(), "operator committed blockhash is not included in header chain"); + assert!( + operator_committed_blockhash == btc_best_block_hash, + "operator committed blockhash is not included in header chain" + ); //operator_public_input (operator_committed_blockhash, constant, included_watchtowers.to_le_bytes::<32>()) diff --git a/node/README.md b/node/README.md index fac78404..ddc22914 100644 --- a/node/README.md +++ b/node/README.md @@ -22,30 +22,49 @@ stateDiagram-v2 [*] --> OperatorPresigned: Create graph OperatorPresigned --> CommitteePresigned: Committee presigns + OperatorPresigned --> Obsoleted: PreKickoff on-chain but data not posted + CommitteePresigned --> OperatorDataPushed: Operator pushes L2 data - OperatorDataPushed --> PreKickoff: PreKickoff tx confirmed + CommitteePresigned --> Obsoleted: PreKickoff on-chain but data not posted + + OperatorDataPushed --> PreKickoff: PreKickoff tx confirmed on Bitcoin + OperatorDataPushed --> Obsoleted: Pegin not withdrawable & no withdraw request PreKickoff --> OperatorKickOff: Kickoff tx broadcast PreKickoff --> Skipped: Guardian/ForceSkip triggered - OperatorKickOff --> Challenge: WatchtowerChallengeInit confirmed OperatorKickOff --> OperatorTake1: Timeout without challenge + OperatorKickOff --> Challenge: WatchtowerChallengeInit confirmed - Challenge --> Disprove: Challenge or timeout detected - Challenge --> OperatorTake1: All resolved normally - - Disprove --> OperatorTake2: Disprove verified + Challenge --> Disprove: Disprove needed
(challenge/timeout detected) + Challenge --> OperatorTake2: Normal completion
(all challenges passed) OperatorTake1 --> [*] OperatorTake2 --> [*] Skipped --> [*] Obsoleted --> [*] + Disprove --> [*] note right of Challenge - Tracked by ChallengeSubStatus: - - watchtower_challenge_status - - commit_blockhash_status - - assert_commit_status + Sub-phases tracked by ChallengeSubStatus: + 1. Watchtower Challenge Phase + - Watchers may challenge + - Operator ACK/NACK responses + 2. CommitBlockHash Phase + - Operator commits blockhash + 3. Assert Commit Phase + - Operator commits assertions + end note + + note right of Disprove + Triggered when any challenge + or timeout detected during + Challenge phase sub-phases + end note + + note right of Obsoleted + Reimbursement by other operators + or graph data not posted in time end note note right of OperatorPresigned @@ -152,7 +171,6 @@ stateDiagram-v2 Challenge --> OperatorACK: Operator accepts challenge claim Challenge --> OperatorNACK: Operator rejects challenge claim - Challenge --> ChallengeTimeout: Operator response timelock expires OperatorACK --> [*] OperatorNACK --> [*] @@ -200,7 +218,7 @@ pub struct WTInitTxVoutMonitorData { ``` - `data_map`: Tracks status for each watchtower index -- `require_disproved_indexes`: Indices requiring disprove (populated when item status changes to ChallengeTimeout or OperatorNACK) +- `require_disproved_indexes`: Indices requiring disprove (populated for items in OperatorInit or Challenge status) - `commit_blockhash_status`: Synchronized with WatchtowerChallengeStatus - `is_challenge_timeout_sent`: Flag for timeout message tracking diff --git a/proof-builder-rpc/src/task/header_chain_proof.rs b/proof-builder-rpc/src/task/header_chain_proof.rs index ee29e9e0..4f663b65 100644 --- a/proof-builder-rpc/src/task/header_chain_proof.rs +++ b/proof-builder-rpc/src/task/header_chain_proof.rs @@ -26,6 +26,11 @@ pub(crate) fn spawn_header_chain_proof_task( anyhow::bail!("Header chain proof generate task cancelled"); } } + assert_eq!( + args.batch_size, 1, + "Header chain proof batch size must be 1, current batch size: {}", + args.batch_size + ); let builder = HeaderChainProofBuilder::new(); loop { From 161f350c850c56a78a3844bc0de3f77314d886c8 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 16:24:49 +0000 Subject: [PATCH 09/12] fix: constrain header chain size to 1 --- node/README.md | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/node/README.md b/node/README.md index ddc22914..485a8e2b 100644 --- a/node/README.md +++ b/node/README.md @@ -75,6 +75,31 @@ stateDiagram-v2 end note ``` +### Challenge to OperatorTake2 Flow + +The path from Challenge → OperatorTake2 requires successful completion of three sub-phases: + +``` +Challenge (GraphStatus) + ↓ +1. WatchtowerChallengeStatus: None → OperatorInit → WatchtowerChallenge → WatchtowerChallengeNormalFinished + + CommitBlockHashStatus: None → WatchtowerChallengeProcessed → OperatorCommit + ↓ +2. AssertCommitStatus: None → OperatorInit → OperatorCommit + ↓ + All three conditions met: + - WatchtowerChallengeNormalFinished ✓ + - CommitBlockHashStatus = OperatorCommit ✓ + - AssertCommitStatus = OperatorCommit ✓ + ↓ +OperatorTake2 +``` + +**Conditions for successful transition to OperatorTake2:** +- All watchtowers acknowledge their challenges (no NACK or timeouts) +- Operator successfully commits the blockhash +- Operator successfully commits all assertions + ### Challenge Phase: WatchtowerChallengeStatus From `src/scheduled_tasks/graph_maintenance_tasks.rs::WatchtowerChallengeStatus`: From 2df00728054928ae633a9cd9b72e305fe7ce2102 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 16:30:20 +0000 Subject: [PATCH 10/12] fix: constrain header chain size to 1 --- node/README.md | 84 +++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 18 deletions(-) diff --git a/node/README.md b/node/README.md index 485a8e2b..924d7702 100644 --- a/node/README.md +++ b/node/README.md @@ -79,26 +79,74 @@ stateDiagram-v2 The path from Challenge → OperatorTake2 requires successful completion of three sub-phases: -``` -Challenge (GraphStatus) - ↓ -1. WatchtowerChallengeStatus: None → OperatorInit → WatchtowerChallenge → WatchtowerChallengeNormalFinished - + CommitBlockHashStatus: None → WatchtowerChallengeProcessed → OperatorCommit - ↓ -2. AssertCommitStatus: None → OperatorInit → OperatorCommit - ↓ - All three conditions met: - - WatchtowerChallengeNormalFinished ✓ - - CommitBlockHashStatus = OperatorCommit ✓ - - AssertCommitStatus = OperatorCommit ✓ - ↓ -OperatorTake2 +```mermaid +--- +title: Challenge Phase Flow to OperatorTake2 +--- +stateDiagram-v2 + [*] --> Challenge + + Challenge --> WTPhase: Enter Watchtower Challenge Phase + Challenge --> Disprove: Challenge/Timeout
detected + + WTPhase --> WTInit: WatchtowerChallengeInitTx
confirmed + WTInit --> WTChallenge: Watchtowers may challenge + WTInit --> WTTimeout: No watchtower
challenges + + WTChallenge --> WTAllACK: All watchtowers ACK + WTChallenge --> Disprove: Any NACK or timeout + + WTTimeout --> Disprove: Timeout expired + + WTAllACK --> BlockHashPhase: Proceed to BlockHash Phase + + BlockHashPhase --> BlockHashWait: Wait for
WatchtowerChallenge
completion + BlockHashWait --> BlockHashCommit: Operator commits
blockhash + BlockHashWait --> Disprove: Commit timeout + + BlockHashCommit --> AssertPhase: Proceed to Assert Phase + + AssertPhase --> AssertInit: AssertInitTx confirmed + AssertInit --> AssertCommit: Operator commits
assertions + AssertInit --> Disprove: Assert timeout + + AssertCommit --> CheckComplete: All phases complete? + + CheckComplete --> OperatorTake2: Yes - All conditions met
✓ WatchtowerChallengeNormalFinished
✓ BlockHash committed
✓ Assertions committed + CheckComplete --> Disprove: No - Missing conditions + + OperatorTake2 --> [*] + Disprove --> [*] + + note right of Challenge + Initial state when + WatchtowerChallengeInit + confirmed on Bitcoin + end note + + note right of WTPhase + Monitor each watchtower + for challenges or timeout + end note + + note right of BlockHashPhase + Only proceeds after + WatchtowerChallengeStatus + reaches normal finish + end note + + note right of AssertPhase + Operator must commit + all assertions within + timelock window + end note ``` -**Conditions for successful transition to OperatorTake2:** -- All watchtowers acknowledge their challenges (no NACK or timeouts) -- Operator successfully commits the blockhash -- Operator successfully commits all assertions +**Transition Conditions to OperatorTake2:** +- `watchtower_challenge_status == WatchtowerChallengeNormalFinished` +- `commit_blockhash_status == OperatorCommit` +- `assert_commit_status == OperatorCommit` +- `disprove_type == None` (no errors detected) ### Challenge Phase: WatchtowerChallengeStatus From 5d326f5824b0470c51dbefd8b6f97bb048eb0ac8 Mon Sep 17 00:00:00 2001 From: eigmax Date: Sun, 18 Jan 2026 16:34:10 +0000 Subject: [PATCH 11/12] fix: constrain header chain size to 1 --- ...ada101f1c951bd846fa7406de6e6ae944046e.json | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 crates/store/.sqlx/query-2cc9579973d79f977833c8e4bd2ada101f1c951bd846fa7406de6e6ae944046e.json diff --git a/crates/store/.sqlx/query-2cc9579973d79f977833c8e4bd2ada101f1c951bd846fa7406de6e6ae944046e.json b/crates/store/.sqlx/query-2cc9579973d79f977833c8e4bd2ada101f1c951bd846fa7406de6e6ae944046e.json new file mode 100644 index 00000000..dd7b3a37 --- /dev/null +++ b/crates/store/.sqlx/query-2cc9579973d79f977833c8e4bd2ada101f1c951bd846fa7406de6e6ae944046e.json @@ -0,0 +1,98 @@ +{ + "db_name": "SQLite", + "query": "SELECT block_start, block_end, chain_name, path_to_proof, public_value_hex, proof_size, cycles, proof_state, total_time_to_proof, proving_time,\n zkm_version, extra, updated_at, created_at FROM long_running_task_proof\n WHERE chain_name = ?", + "describe": { + "columns": [ + { + "name": "block_start", + "ordinal": 0, + "type_info": "Integer" + }, + { + "name": "block_end", + "ordinal": 1, + "type_info": "Integer" + }, + { + "name": "chain_name", + "ordinal": 2, + "type_info": "Text" + }, + { + "name": "path_to_proof", + "ordinal": 3, + "type_info": "Text" + }, + { + "name": "public_value_hex", + "ordinal": 4, + "type_info": "Text" + }, + { + "name": "proof_size", + "ordinal": 5, + "type_info": "Integer" + }, + { + "name": "cycles", + "ordinal": 6, + "type_info": "Integer" + }, + { + "name": "proof_state", + "ordinal": 7, + "type_info": "Integer" + }, + { + "name": "total_time_to_proof", + "ordinal": 8, + "type_info": "Integer" + }, + { + "name": "proving_time", + "ordinal": 9, + "type_info": "Integer" + }, + { + "name": "zkm_version", + "ordinal": 10, + "type_info": "Text" + }, + { + "name": "extra", + "ordinal": 11, + "type_info": "Text" + }, + { + "name": "updated_at", + "ordinal": 12, + "type_info": "Integer" + }, + { + "name": "created_at", + "ordinal": 13, + "type_info": "Integer" + } + ], + "parameters": { + "Right": 1 + }, + "nullable": [ + false, + false, + false, + true, + true, + false, + false, + false, + false, + false, + false, + true, + false, + false + ] + }, + "hash": "2cc9579973d79f977833c8e4bd2ada101f1c951bd846fa7406de6e6ae944046e" +} From a69bd86fd3706c5b9826d7fb79d366366342f837 Mon Sep 17 00:00:00 2001 From: eigmax Date: Mon, 19 Jan 2026 00:27:13 +0000 Subject: [PATCH 12/12] fix: move funtion before test --- node/src/utils.rs | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/node/src/utils.rs b/node/src/utils.rs index 29860041..42a7ffd5 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -4555,27 +4555,6 @@ pub(crate) async fn get_bridge_out_global_stats<'a>( } } -#[cfg(test)] -mod tests { - use super::*; - - #[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:#?}"); - } -} - pub async fn get_largest_watchtower_challenge_block( graph: &Bitvm2Graph, btc_client: &BTCClient, @@ -4623,3 +4602,24 @@ pub async fn get_largest_watchtower_challenge_block( } Ok(largest_watchtower_challenge_block_hash) } + +#[cfg(test)] +mod tests { + use super::*; + + #[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:#?}"); + } +}