From 0804f90f415db377291704f3fac406456616bb66 Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Tue, 14 Jul 2026 23:09:21 +0800 Subject: [PATCH 01/11] decode prev state from public values --- Cargo.lock | 2 + circuits/commit-chain-proof/host/src/lib.rs | 10 ++-- circuits/header-chain-proof/guest/Cargo.lock | 1 + circuits/header-chain-proof/host/src/lib.rs | 9 ++-- circuits/operator-proof/guest/Cargo.lock | 2 + circuits/state-chain-proof/guest/Cargo.lock | 2 + circuits/state-chain-proof/host/src/lib.rs | 11 ++-- circuits/watchtower-proof/guest/Cargo.lock | 2 + .../bitcoin-light-client-circuit/src/lib.rs | 50 ++++++------------- crates/commit-chain/src/commit_chain.rs | 2 +- crates/commit-chain/src/lib.rs | 5 +- crates/header-chain/Cargo.toml | 1 + crates/header-chain/src/header_chain.rs | 2 +- crates/header-chain/src/lib.rs | 8 ++- crates/state-chain/Cargo.toml | 1 + crates/state-chain/src/lib.rs | 8 ++- crates/state-chain/src/state_chain.rs | 2 +- 17 files changed, 56 insertions(+), 62 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fb7561fb9..d16338738 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6029,6 +6029,7 @@ dependencies = [ "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -12476,6 +12477,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index e5fce4076..f828d4817 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -206,9 +206,8 @@ impl ProofBuilder for CommitChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - let prev_output = decode_commit_chain_circuit_output(&public_inputs); ( - CommitChainPrevProofType::PrevProof(prev_output), + CommitChainPrevProofType::PrevProof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), @@ -264,10 +263,9 @@ impl ProofBuilder for CommitChainProofBuilder { tracing::info!("Commit chain proof cycles: {}", cycles); - // todo: verify the proof laterr - // if let Err(e) = self.client.verify(&proof, &self.verifying_key) { - // panic!("{}", e); - // } + self.client + .verify(&proof, &self.verifying_key) + .context("Failed to verify generated commit chain proof")?; let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) diff --git a/circuits/header-chain-proof/guest/Cargo.lock b/circuits/header-chain-proof/guest/Cargo.lock index cd02d2928..a0cec3ae1 100644 --- a/circuits/header-chain-proof/guest/Cargo.lock +++ b/circuits/header-chain-proof/guest/Cargo.lock @@ -1117,6 +1117,7 @@ dependencies = [ "serde", "sha2", "verifier", + "zkm-primitives", "zkm-zkvm", ] diff --git a/circuits/header-chain-proof/host/src/lib.rs b/circuits/header-chain-proof/host/src/lib.rs index e107db823..8f762def7 100644 --- a/circuits/header-chain-proof/host/src/lib.rs +++ b/circuits/header-chain-proof/host/src/lib.rs @@ -222,9 +222,8 @@ impl ProofBuilder for HeaderChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - let prev_output = zkm_sdk::ZKMPublicValues::from(&public_inputs).read(); ( - HeaderChainPrevProofType::PrevProof(prev_output), + HeaderChainPrevProofType::PrevProof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), @@ -285,9 +284,9 @@ impl ProofBuilder for HeaderChainProofBuilder { tracing::info!("Header chain proof cycles: {}", cycles); - if let Err(e) = self.client.verify(&proof, &self.verifying_key) { - panic!("{}", e); - } + self.client + .verify(&proof, &self.verifying_key) + .context("Failed to verify generated header chain proof")?; let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) diff --git a/circuits/operator-proof/guest/Cargo.lock b/circuits/operator-proof/guest/Cargo.lock index 91821b9a1..ff3314ba1 100644 --- a/circuits/operator-proof/guest/Cargo.lock +++ b/circuits/operator-proof/guest/Cargo.lock @@ -2434,6 +2434,7 @@ dependencies = [ "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5186,6 +5187,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] diff --git a/circuits/state-chain-proof/guest/Cargo.lock b/circuits/state-chain-proof/guest/Cargo.lock index 226658301..1f3740687 100644 --- a/circuits/state-chain-proof/guest/Cargo.lock +++ b/circuits/state-chain-proof/guest/Cargo.lock @@ -2383,6 +2383,7 @@ dependencies = [ "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5135,6 +5136,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] diff --git a/circuits/state-chain-proof/host/src/lib.rs b/circuits/state-chain-proof/host/src/lib.rs index b29a6a360..115e0c0bf 100644 --- a/circuits/state-chain-proof/host/src/lib.rs +++ b/circuits/state-chain-proof/host/src/lib.rs @@ -336,10 +336,8 @@ impl ProofBuilder for StateChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - let prev_output: StateChainCircuitOutput = - zkm_sdk::ZKMPublicValues::from(&public_inputs).read(); ( - StateChainPrevProofType::PrevProof(prev_output), + StateChainPrevProofType::PrevProof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), @@ -390,9 +388,10 @@ impl ProofBuilder for StateChainProofBuilder { }, )?; tracing::info!("State chain proof cycles: {}", cycles); - if let Err(e) = self.client.verify(&proof, &self.verifying_key) { - panic!("{}", e); - } + + self.client + .verify(&proof, &self.verifying_key) + .context("Failed to verify generated state chain proof")?; let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) diff --git a/circuits/watchtower-proof/guest/Cargo.lock b/circuits/watchtower-proof/guest/Cargo.lock index 0dd35c975..b889d4717 100644 --- a/circuits/watchtower-proof/guest/Cargo.lock +++ b/circuits/watchtower-proof/guest/Cargo.lock @@ -2434,6 +2434,7 @@ dependencies = [ "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5186,6 +5187,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index 9cb38b9f7..e61efba59 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -2,21 +2,20 @@ mod signature; mod utils; use alloy_primitives::U32; pub use signature::*; -use state_chain::verify_sequencer_commit; pub use utils::*; use alloy_primitives::U256; use bitcoin::Block; use bitcoin::hashes::{Hash, HashEngine, sha256}; use commit_chain::{ - CommitChainCircuitInput, CommitChainPrevProofType, decode_commit_chain_circuit_output, + CommitChainCircuitInput, 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, + BitcoinMerkleTree, BlockHeaderCircuitOutput, CircuitBlockHeader, CircuitTransaction, + HeaderChainCircuitInput, MMRHost, SPV, verify_merkle_proof, }; -use state_chain::{StateChainCircuitInput, StateChainPrevProofType}; +use state_chain::{StateChainCircuitInput, StateChainCircuitOutput, verify_sequencer_commit}; use zkm_primitives::io::ZKMPublicValues; use bitcoin::{ @@ -83,11 +82,7 @@ pub fn watch_longest_chain( ) .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"); - }; + let commit_chain_output = decode_commit_chain_circuit_output(&commit_chain.zkm_public_values); assert_eq!( commit_chain_output.chain_state.commit_txn.compute_txid(), @@ -105,11 +100,8 @@ pub fn watch_longest_chain( ) .expect("Failed to verify header chain proof"); - let prev_output = ZKMPublicValues::from(&header_chain.zkm_public_values).read(); - let prev_proof = HeaderChainPrevProofType::PrevProof(prev_output); - let HeaderChainPrevProofType::PrevProof(btc_header_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in watch_longest_chain"); - }; + let btc_header_chain_output: BlockHeaderCircuitOutput = + ZKMPublicValues::from(&header_chain.zkm_public_values).read(); // verify that the latest_sequecner_commit_tx is in the header chain println!("SPV"); assert!(spv.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); @@ -121,11 +113,8 @@ pub fn watch_longest_chain( &state_chain.zkm_version, ) .expect("Failed to verify state chain proof"); - let prev_output = ZKMPublicValues::from(&state_chain.zkm_public_values).read(); - let prev_proof = StateChainPrevProofType::PrevProof(prev_output); - let StateChainPrevProofType::PrevProof(state_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in watch_longest_chain"); - }; + let state_chain_output: StateChainCircuitOutput = + ZKMPublicValues::from(&state_chain.zkm_public_values).read(); // check the signature. let cosmos_block_bytes = &state_chain_output.chain_state.latest_cosmos_block; let cosmos_block: LightBlock = @@ -255,11 +244,7 @@ pub fn propose_longest_chain( &commit_chain.zkm_version, ) .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"); - }; + let commit_chain_output = decode_commit_chain_circuit_output(&commit_chain.zkm_public_values); assert_eq!( commit_chain_output.chain_state.commit_txn.compute_txid(), spv_ss_commit.transaction.0.compute_txid() @@ -278,11 +263,8 @@ pub fn propose_longest_chain( &operator_header_chain.zkm_version, ) .expect("Failed to verify header chain proof"); - let prev_output = ZKMPublicValues::from(&operator_header_chain.zkm_public_values).read(); - let prev_proof = HeaderChainPrevProofType::PrevProof(prev_output); - let HeaderChainPrevProofType::PrevProof(btc_header_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in propose_longest_chain"); - }; + let btc_header_chain_output: BlockHeaderCircuitOutput = + ZKMPublicValues::from(&operator_header_chain.zkm_public_values).read(); let operator_total_work = btc_header_chain_output.chain_state.total_work; let operator_consensus_block_height = U32::from(commit_chain_output.chain_state.block_height); // commit header chain best block hash as pis @@ -429,11 +411,9 @@ pub fn propose_longest_chain( ) .expect("Failed to verify state chain proof"); - let prev_output = ZKMPublicValues::from(&state_chain.zkm_public_values).read(); - let prev_proof = StateChainPrevProofType::PrevProof(prev_output); - let StateChainPrevProofType::PrevProof(state_chain_output) = &prev_proof else { - panic!("Only PrevProof is supported in propose_longest_chain"); - }; + let state_chain_output: StateChainCircuitOutput = + ZKMPublicValues::from(&state_chain.zkm_public_values).read(); + // check the signature. let cosmos_block_bytes = &state_chain_output.chain_state.latest_cosmos_block; let cosmos_block: LightBlock = diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index 4b8c5a087..212245acc 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -29,7 +29,7 @@ pub struct CommitInfo { #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub enum CommitChainPrevProofType { GenesisBlock, - PrevProof(CommitChainCircuitOutput), + PrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] diff --git a/crates/commit-chain/src/lib.rs b/crates/commit-chain/src/lib.rs index bee13b09c..e17369129 100644 --- a/crates/commit-chain/src/lib.rs +++ b/crates/commit-chain/src/lib.rs @@ -8,7 +8,7 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui CommitChainPrevProofType::GenesisBlock => { CommitChainState::new(input.commits[0].genesis_txid) } - CommitChainPrevProofType::PrevProof(prev_proof) => { + CommitChainPrevProofType::PrevProof => { println!("verify commit chain of prev proof"); verifier::verify_groth16_proof( &input.zkm_proof, @@ -18,8 +18,7 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui ) .unwrap(); - // todo: read from input.zkm_public_values - prev_proof.chain_state + decode_commit_chain_circuit_output(&input.zkm_public_values).chain_state } }; diff --git a/crates/header-chain/Cargo.toml b/crates/header-chain/Cargo.toml index d395bb179..12f18cec8 100644 --- a/crates/header-chain/Cargo.toml +++ b/crates/header-chain/Cargo.toml @@ -12,6 +12,7 @@ serde = { workspace = true, default-features = false } crypto-bigint = { version = "0.5.5", default-features = false } zkm-zkvm = { workspace = true } verifier = { workspace = true } +zkm-primitives = { 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 a36e91824..78ea24b06 100644 --- a/crates/header-chain/src/header_chain.rs +++ b/crates/header-chain/src/header_chain.rs @@ -370,7 +370,7 @@ pub struct BlockHeaderCircuitOutput { #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, BorshDeserialize, BorshSerialize)] pub enum HeaderChainPrevProofType { GenesisBlock, - PrevProof(BlockHeaderCircuitOutput), + PrevProof, } /// The input of the header chain circuit. diff --git a/crates/header-chain/src/lib.rs b/crates/header-chain/src/lib.rs index 0617922fa..142335f06 100644 --- a/crates/header-chain/src/lib.rs +++ b/crates/header-chain/src/lib.rs @@ -10,6 +10,8 @@ pub use merkle_tree::*; pub use mmr::*; pub use transaction::*; +use zkm_primitives::io::ZKMPublicValues; + pub mod spv; pub use spv::SPV; @@ -19,7 +21,7 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui // println!("NETWORK_CONSTANTS: {:?}", NETWORK_CONSTANTS); let mut chain_state = match input.prev_proof { HeaderChainPrevProofType::GenesisBlock => ChainState::new(), - HeaderChainPrevProofType::PrevProof(prev_proof) => { + HeaderChainPrevProofType::PrevProof => { println!("verify header chain of prev proof"); verifier::verify_groth16_proof( &input.zkm_proof, @@ -29,7 +31,9 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui ) .unwrap(); - prev_proof.chain_state + let btc_header_chain_output: BlockHeaderCircuitOutput = + ZKMPublicValues::from(&input.zkm_public_values).read(); + btc_header_chain_output.chain_state } }; diff --git a/crates/state-chain/Cargo.toml b/crates/state-chain/Cargo.toml index 8e98a5701..d810a5326 100644 --- a/crates/state-chain/Cargo.toml +++ b/crates/state-chain/Cargo.toml @@ -15,6 +15,7 @@ header-chain = { path = "../header-chain" } # Ziren verifier = { workspace = true } zkm-zkvm = { workspace = true } +zkm-primitives = { workspace = true } #zkm-verifier = { path = "../../../Ziren/crates/verifier" } #zkm-zkvm = { path = "../../../Ziren/crates/zkvm/entrypoint", features = ["verify"] } diff --git a/crates/state-chain/src/lib.rs b/crates/state-chain/src/lib.rs index 0fe26513e..1a55b0b7a 100644 --- a/crates/state-chain/src/lib.rs +++ b/crates/state-chain/src/lib.rs @@ -4,6 +4,8 @@ mod state_chain; pub use cbft::*; pub use state_chain::*; +use zkm_primitives::io::ZKMPublicValues; + pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOutput { let mut chain_state = match input.prev_proof { StateChainPrevProofType::GenesisBlock => { @@ -12,7 +14,7 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu let cosmos_block = input.blocks[0].cosmos_block.clone(); StateChainState::new(block_height, block_hash, cosmos_block) } - StateChainPrevProofType::PrevProof(prev_proof) => { + StateChainPrevProofType::PrevProof => { println!("verify state chain of prev proof"); verifier::verify_groth16_proof( &input.zkm_proof, @@ -22,7 +24,9 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu ) .unwrap(); - prev_proof.chain_state + let state_chain_output: StateChainCircuitOutput = + ZKMPublicValues::from(&input.zkm_public_values).read(); + state_chain_output.chain_state } }; diff --git a/crates/state-chain/src/state_chain.rs b/crates/state-chain/src/state_chain.rs index 473db11e5..3c9f3fe38 100644 --- a/crates/state-chain/src/state_chain.rs +++ b/crates/state-chain/src/state_chain.rs @@ -16,7 +16,7 @@ type WithdrawalSlot = (Address, [u8; 32], Vec<[u8; 16]>); #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub enum StateChainPrevProofType { GenesisBlock, - PrevProof(StateChainCircuitOutput), + PrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] From ddaeeda3f924bc9ffeb987020df5ae862ead111d Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Wed, 15 Jul 2026 00:37:12 +0800 Subject: [PATCH 02/11] fix(spv): remove latest_sequencer_commit_txid argument from watch_longest_chain --- circuits/watchtower-proof/guest/src/main.rs | 2 -- circuits/watchtower-proof/host/src/lib.rs | 3 --- crates/bitcoin-light-client-circuit/src/lib.rs | 6 ++---- 3 files changed, 2 insertions(+), 9 deletions(-) diff --git a/circuits/watchtower-proof/guest/src/main.rs b/circuits/watchtower-proof/guest/src/main.rs index 2a5431b7a..8862f750d 100644 --- a/circuits/watchtower-proof/guest/src/main.rs +++ b/circuits/watchtower-proof/guest/src/main.rs @@ -8,7 +8,6 @@ use state_chain::StateChainCircuitInput; pub fn main() { let genesis_sequencer_commit_txid = zkm_zkvm::io::read::<[u8; 32]>(); - let latest_sequencer_commit_txid = zkm_zkvm::io::read::<[u8; 32]>(); let header_chain: HeaderChainCircuitInput = zkm_zkvm::io::read(); // private inputs let commit_chain: CommitChainCircuitInput = zkm_zkvm::io::read(); let state_chain: StateChainCircuitInput = zkm_zkvm::io::read(); @@ -16,7 +15,6 @@ pub fn main() { 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, diff --git a/circuits/watchtower-proof/host/src/lib.rs b/circuits/watchtower-proof/host/src/lib.rs index e0023bc0e..d488cc8a2 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -122,7 +122,6 @@ impl ProofBuilder for WatchtowerProofBuilder { header_chain_input_proof, commit_chain_input_proof, state_chain_input_proof, - latest_sequencer_commit_txid, genesis_sequencer_commit_txid, target_block, block_pos, @@ -215,7 +214,6 @@ 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 bitcoin_block_headers = { let headers: Vec = std::fs::read(format!("{header_chain_input_proof}.blocks"))?; headers @@ -247,7 +245,6 @@ impl ProofBuilder for WatchtowerProofBuilder { || -> anyhow::Result<(ZKMProofWithPublicValues, u64, f32)> { let mut stdin = ZKMStdin::new(); stdin.write(&genesis_sequencer_commit_txid.to_byte_array()); - stdin.write(&latest_sequencer_commit_txid.to_byte_array()); stdin.write(&header_chain_input); stdin.write(&commit_chain_input); stdin.write(&state_chain_input); diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index e61efba59..8dc6b808c 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -19,7 +19,7 @@ use state_chain::{StateChainCircuitInput, StateChainCircuitOutput, verify_sequen use zkm_primitives::io::ZKMPublicValues; use bitcoin::{ - ScriptBuf, Transaction, TxOut, Txid, + ScriptBuf, Transaction, TxOut, secp256k1::{PublicKey, XOnlyPublicKey}, }; pub use guest_executor::io::EthClientExecutorInput; @@ -63,7 +63,6 @@ pub fn decode_operator_public_outputs( pub fn watch_longest_chain( genesis_sequencer_commit_txid: [u8; 32], - latest_sequencer_commit_txid: [u8; 32], header_chain: HeaderChainCircuitInput, commit_chain: CommitChainCircuitInput, state_chain: StateChainCircuitInput, @@ -83,10 +82,9 @@ pub fn watch_longest_chain( .expect("Failed to verify commit chain proof"); let commit_chain_output = decode_commit_chain_circuit_output(&commit_chain.zkm_public_values); - assert_eq!( commit_chain_output.chain_state.commit_txn.compute_txid(), - Txid::from_byte_array(latest_sequencer_commit_txid) + spv.transaction.0.compute_txid() ); assert_eq!(genesis_sequencer_commit_txid, commit_chain_output.chain_state.genesis_txid); From 598803ef62de634ae2b4b693f36993aaf5b1487a Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Wed, 15 Jul 2026 10:23:28 +0800 Subject: [PATCH 03/11] fix(vk): ensure Groth16 verifying key matches immutable vk --- node/src/vk.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/node/src/vk.rs b/node/src/vk.rs index 5e5667ce0..bac061240 100644 --- a/node/src/vk.rs +++ b/node/src/vk.rs @@ -1,10 +1,10 @@ #![allow(dead_code)] -use anyhow::Result; +use anyhow::{Result, ensure}; use std::fs; use std::path::PathBuf; use zkm_sdk::install::CIRCUIT_ARTIFACTS_URL_BASE; -use zkm_verifier::load_ark_groth16_verifying_key_from_bytes; +use zkm_verifier::{IMM_GROTH16_VK_BYTES, load_ark_groth16_verifying_key_from_bytes}; use { futures::StreamExt, @@ -31,6 +31,12 @@ pub async fn get_vk() -> Result { let build_dir = try_install_circuit_artifacts(); let vk_file = build_dir.join("groth16_vk.bin"); let content = fs::read(&vk_file)?; + ensure!( + content.as_slice() == *IMM_GROTH16_VK_BYTES, + "Groth16 verifying key at {} does not match embedded IMM Groth16 verifying key", + vk_file.display() + ); + Ok(load_ark_groth16_verifying_key_from_bytes(&content)?) } From aa94d509add3b3dbf2ffabb5ac718d0761057e73 Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Mon, 20 Jul 2026 17:56:46 +0800 Subject: [PATCH 04/11] fix(vk): bind recursive proofs to authenticated ProgramId history --- Cargo.lock | 10 + circuits/commit-chain-proof/guest/Cargo.lock | 1 + circuits/commit-chain-proof/host/Cargo.toml | 1 + circuits/commit-chain-proof/host/src/lib.rs | 27 ++- circuits/commit-chain-proof/host/src/main.rs | 8 +- circuits/header-chain-proof/guest/Cargo.lock | 2 + circuits/header-chain-proof/host/Cargo.toml | 1 + circuits/header-chain-proof/host/src/lib.rs | 33 +++- circuits/header-chain-proof/host/src/main.rs | 8 +- circuits/operator-proof/guest/Cargo.lock | 2 + circuits/operator-proof/guest/src/main.rs | 7 + circuits/operator-proof/host/Cargo.toml | 1 + circuits/operator-proof/host/src/lib.rs | 9 + circuits/state-chain-proof/guest/Cargo.lock | 2 + circuits/state-chain-proof/host/Cargo.toml | 1 + circuits/state-chain-proof/host/src/lib.rs | 32 +++- circuits/state-chain-proof/host/src/main.rs | 8 +- circuits/watchtower-proof/guest/Cargo.lock | 2 + circuits/watchtower-proof/host/Cargo.toml | 1 + circuits/watchtower-proof/host/src/lib.rs | 24 ++- circuits/watchtower-proof/host/src/main.rs | 8 +- .../bitcoin-light-client-circuit/src/lib.rs | 135 +++++++++++++- .../bitcoin-light-client-circuit/src/utils.rs | 2 +- crates/commit-chain/src/commit_chain.rs | 97 +++++++++- crates/commit-chain/src/lib.rs | 78 +++++++- crates/header-chain/Cargo.toml | 1 + crates/header-chain/src/header_chain.rs | 27 +++ crates/header-chain/src/lib.rs | 74 +++++++- crates/state-chain/src/lib.rs | 77 +++++++- crates/state-chain/src/state_chain.rs | 27 +++ crates/verifier/Cargo.toml | 3 +- crates/verifier/src/lib.rs | 95 +++++++++- node/Cargo.toml | 3 + node/src/bin/sequencer-set-publish.rs | 175 +++++++++++++++++- node/src/handle.rs | 10 +- 35 files changed, 893 insertions(+), 99 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d16338738..537d2228d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3100,6 +3100,7 @@ dependencies = [ "esplora-client", "futures", "goat", + "header-chain", "hex", "http 1.4.0", "http-body-util", @@ -3120,6 +3121,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.10.9", + "state-chain", "store", "strum 0.26.3", "stun-client", @@ -3133,6 +3135,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "util", "uuid 1.23.0", + "verifier", "zeroize", "zkm-recursion-core", "zkm-sdk", @@ -3750,6 +3753,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber 0.3.23", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -6022,6 +6026,7 @@ dependencies = [ name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", @@ -6058,6 +6063,7 @@ dependencies = [ "tracing", "tracing-subscriber 0.3.23", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -8642,6 +8648,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "url", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -12515,6 +12522,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "url", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -13936,6 +13944,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] @@ -14155,6 +14164,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber 0.3.23", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", diff --git a/circuits/commit-chain-proof/guest/Cargo.lock b/circuits/commit-chain-proof/guest/Cargo.lock index 460084d2d..61a4d9f46 100644 --- a/circuits/commit-chain-proof/guest/Cargo.lock +++ b/circuits/commit-chain-proof/guest/Cargo.lock @@ -5652,6 +5652,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] diff --git a/circuits/commit-chain-proof/host/Cargo.toml b/circuits/commit-chain-proof/host/Cargo.toml index 53e946ce4..c2c48a0c1 100644 --- a/circuits/commit-chain-proof/host/Cargo.toml +++ b/circuits/commit-chain-proof/host/Cargo.toml @@ -25,6 +25,7 @@ tendermint-light-client-verifier = { workspace = true, default-features = false, ] } proof-builder.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index f828d4817..8c6ab57c4 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -4,8 +4,8 @@ use commit_chain::*; use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; use std::str::FromStr; use zkm_sdk::{ - HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, - include_elf, + HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofKind, ZKMProofWithPublicValues, + ZKMStdin, include_elf, }; use sha2::{Digest, Sha256}; @@ -23,6 +23,10 @@ use clap::Parser; /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -150,6 +154,11 @@ impl CommitChainProofBuilder { let (proving_key, verifying_key) = client.setup(COMMIT_CHAIN); Self { client, proving_key, verifying_key } } + + pub fn program_id(&self) -> anyhow::Result { + verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } } impl ProofBuilder for CommitChainProofBuilder { @@ -189,6 +198,7 @@ impl ProofBuilder for CommitChainProofBuilder { //let prev: CommitChainCircuitOutput = serde_json::from_slice(&public_inputs).unwrap(); Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { @@ -206,20 +216,16 @@ impl ProofBuilder for CommitChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - ( - CommitChainPrevProofType::PrevProof, - proof_bytes, - public_inputs, - zkm_vk_hash.to_vec(), - zkm_version, - ) + let prev_proof = + classify_commit_chain_output(&public_inputs).map_err(anyhow::Error::msg)?; + (prev_proof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), zkm_version) } None => ( CommitChainPrevProofType::GenesisBlock, Vec::new(), Vec::new(), Vec::new(), - "v1.2.5".into(), + ZKM_CIRCUIT_VERSION.into(), ), }; @@ -228,6 +234,7 @@ impl ProofBuilder for CommitChainProofBuilder { zkm_version, zkm_proof, prev_proof, + self_program_id, commits: commits.to_vec(), zkm_public_values, }; diff --git a/circuits/commit-chain-proof/host/src/main.rs b/circuits/commit-chain-proof/host/src/main.rs index c6fd0344b..ab72bbe96 100644 --- a/circuits/commit-chain-proof/host/src/main.rs +++ b/circuits/commit-chain-proof/host/src/main.rs @@ -11,6 +11,12 @@ async fn main() { zkm_sdk::utils::setup_logger(); tracing::info!("args: {:?}", args); + let builder = CommitChainProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let commits = fetch_commit_chain( &args.esplora_url, &args.commit_info, @@ -19,8 +25,6 @@ async fn main() { ) .await .unwrap(); - let builder = CommitChainProofBuilder::new(); - let ctx = ProofRequest::CommitChainProofRequest { init_input: args.init_input, input_proof: args.input_proof.clone(), diff --git a/circuits/header-chain-proof/guest/Cargo.lock b/circuits/header-chain-proof/guest/Cargo.lock index a0cec3ae1..4aee28cd5 100644 --- a/circuits/header-chain-proof/guest/Cargo.lock +++ b/circuits/header-chain-proof/guest/Cargo.lock @@ -1111,6 +1111,7 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", @@ -2774,6 +2775,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2", "zkm-verifier", ] diff --git a/circuits/header-chain-proof/host/Cargo.toml b/circuits/header-chain-proof/host/Cargo.toml index 8c98b6a42..aacbab3eb 100644 --- a/circuits/header-chain-proof/host/Cargo.toml +++ b/circuits/header-chain-proof/host/Cargo.toml @@ -29,6 +29,7 @@ bitcoin = { workspace = true } client = { workspace = true } proof-builder = { workspace = true } util = { workspace = true } +verifier = { workspace = true } # Ziren zkm-sdk.workspace = true diff --git a/circuits/header-chain-proof/host/src/lib.rs b/circuits/header-chain-proof/host/src/lib.rs index 8f762def7..213a7a67d 100644 --- a/circuits/header-chain-proof/host/src/lib.rs +++ b/circuits/header-chain-proof/host/src/lib.rs @@ -1,7 +1,10 @@ use bitcoin::Network; use borsh::{BorshDeserialize, BorshSerialize}; use client::btc_chain::BTCClient; -use header_chain::{CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType}; +use header_chain::{ + CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType, + classify_header_chain_output, +}; use proof_builder::{LongRunning, ProofBuilder, ProofRequest}; use sha2::{Digest, Sha256}; use std::{ @@ -10,7 +13,10 @@ use std::{ }; use util::get_btc_block_confirms; use zkm_sdk::ZKMProofKind; -use zkm_sdk::{HashableKey, Prover, ProverClient, ZKMProofWithPublicValues, ZKMStdin, include_elf}; +use zkm_sdk::{ + HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofWithPublicValues, ZKMStdin, + include_elf, +}; static ELF_ID: OnceLock = OnceLock::new(); use anyhow::Context; use clap::Parser; @@ -19,6 +25,10 @@ use std::sync::OnceLock; /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -158,6 +168,11 @@ impl HeaderChainProofBuilder { let (proving_key, verifying_key) = client.setup(HEADER_CHAIN); Self { client, proving_key, verifying_key } } + + pub fn program_id(&self) -> anyhow::Result { + verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } } impl ProofBuilder for HeaderChainProofBuilder { @@ -206,6 +221,7 @@ impl ProofBuilder for HeaderChainProofBuilder { Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { @@ -222,20 +238,16 @@ impl ProofBuilder for HeaderChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - ( - HeaderChainPrevProofType::PrevProof, - proof_bytes, - public_inputs, - zkm_vk_hash.to_vec(), - zkm_version, - ) + let prev_proof = + classify_header_chain_output(&public_inputs).map_err(anyhow::Error::msg)?; + (prev_proof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), zkm_version) } None => ( HeaderChainPrevProofType::GenesisBlock, Vec::new(), Vec::new(), Vec::new(), - "v1.2.5".into(), + ZKM_CIRCUIT_VERSION.into(), ), }; @@ -253,6 +265,7 @@ impl ProofBuilder for HeaderChainProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers, }; diff --git a/circuits/header-chain-proof/host/src/main.rs b/circuits/header-chain-proof/host/src/main.rs index 0c4db794a..b128457f9 100644 --- a/circuits/header-chain-proof/host/src/main.rs +++ b/circuits/header-chain-proof/host/src/main.rs @@ -12,6 +12,12 @@ async fn main() { zkm_sdk::utils::setup_logger(); tracing::info!("args: {args:?}"); + let builder = HeaderChainProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let total_block_headers = fetch_header_chain( &args.esplora_url, args.start, @@ -23,8 +29,6 @@ async fn main() { .await .unwrap(); - let builder = HeaderChainProofBuilder::new(); - let ctx = ProofRequest::HeaderChainProofRequest { init_input: args.init_input, input_proof: args.input_proof.clone(), diff --git a/circuits/operator-proof/guest/Cargo.lock b/circuits/operator-proof/guest/Cargo.lock index ff3314ba1..5ebbd2d98 100644 --- a/circuits/operator-proof/guest/Cargo.lock +++ b/circuits/operator-proof/guest/Cargo.lock @@ -2428,6 +2428,7 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", @@ -5760,6 +5761,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index 7431c5a46..fd3559423 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -8,6 +8,12 @@ use header_chain::{HeaderChainCircuitInput, SPV}; use state_chain::StateChainCircuitInput; use std::str::FromStr; +// Regenerate this ID after changing the Watchtower guest. +const EXPECTED_WATCHTOWER_PROGRAM_ID: [u8; 32] = [ + 0x84, 0xd5, 0x54, 0x57, 0x78, 0x53, 0xb3, 0xad, 0x73, 0x36, 0xee, 0xd8, 0xbf, 0x0a, 0x53, 0x0b, + 0x12, 0x69, 0x1a, 0xa2, 0xe5, 0x2f, 0xd5, 0xe8, 0x49, 0xa2, 0x08, 0x2a, 0xcf, 0xdd, 0xe1, 0x4f, +]; + 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::(); @@ -42,6 +48,7 @@ pub fn main() { watchtower_challenge_txn_scripts, watchtower_challenge_txn_prev_outs, &graph_watchtower_xonly_public_keys, + EXPECTED_WATCHTOWER_PROGRAM_ID, operator_header_chain, operator_commit_chain, operator_state_chain, diff --git a/circuits/operator-proof/host/Cargo.toml b/circuits/operator-proof/host/Cargo.toml index 186054947..7b22071a1 100644 --- a/circuits/operator-proof/host/Cargo.toml +++ b/circuits/operator-proof/host/Cargo.toml @@ -43,6 +43,7 @@ cbft-rpc = { workspace = true } bitcoin = { workspace = true } client = { workspace = true } proof-builder.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 5c62a4ae8..17bff9c09 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -334,12 +334,15 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; HeaderChainCircuitInput { prev_proof: HeaderChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers: vec![], } }; @@ -361,12 +364,15 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; CommitChainCircuitInput { prev_proof: CommitChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, commits: vec![], } }; @@ -387,6 +393,8 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; StateChainCircuitInput { prev_proof: StateChainPrevProofType::GenesisBlock, // unused @@ -394,6 +402,7 @@ impl ProofBuilder for OperatorProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: vec![], } }; diff --git a/circuits/state-chain-proof/guest/Cargo.lock b/circuits/state-chain-proof/guest/Cargo.lock index 1f3740687..7084b519b 100644 --- a/circuits/state-chain-proof/guest/Cargo.lock +++ b/circuits/state-chain-proof/guest/Cargo.lock @@ -2377,6 +2377,7 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", @@ -5709,6 +5710,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] diff --git a/circuits/state-chain-proof/host/Cargo.toml b/circuits/state-chain-proof/host/Cargo.toml index da7404d89..3178e1e98 100644 --- a/circuits/state-chain-proof/host/Cargo.toml +++ b/circuits/state-chain-proof/host/Cargo.toml @@ -35,6 +35,7 @@ cbft-rpc.workspace = true proof-builder.workspace = true alloy-consensus.workspace = true util.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/state-chain-proof/host/src/lib.rs b/circuits/state-chain-proof/host/src/lib.rs index 115e0c0bf..0daaa2557 100644 --- a/circuits/state-chain-proof/host/src/lib.rs +++ b/circuits/state-chain-proof/host/src/lib.rs @@ -17,8 +17,8 @@ use state_chain::*; use std::sync::Arc; use url::Url; use zkm_sdk::{ - HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, - include_elf, + HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofKind, ZKMProofWithPublicValues, + ZKMStdin, include_elf, }; use sha2::{Digest, Sha256}; @@ -36,6 +36,10 @@ use clap::Parser; /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -282,6 +286,11 @@ impl StateChainProofBuilder { let (proving_key, verifying_key) = client.setup(STATE_CHAIN); Self { client, proving_key, verifying_key } } + + pub fn program_id(&self) -> anyhow::Result { + verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } } impl ProofBuilder for StateChainProofBuilder { @@ -319,6 +328,7 @@ impl ProofBuilder for StateChainProofBuilder { Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { @@ -336,20 +346,16 @@ impl ProofBuilder for StateChainProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; - ( - StateChainPrevProofType::PrevProof, - proof_bytes, - public_inputs, - zkm_vk_hash.to_vec(), - zkm_version, - ) + let prev_proof = + classify_state_chain_output(&public_inputs).map_err(anyhow::Error::msg)?; + (prev_proof, proof_bytes, public_inputs, zkm_vk_hash.to_vec(), zkm_version) } None => ( StateChainPrevProofType::GenesisBlock, Vec::new(), Vec::new(), Vec::new(), - "v1.2.5".into(), + ZKM_CIRCUIT_VERSION.into(), ), }; @@ -359,6 +365,7 @@ impl ProofBuilder for StateChainProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: blocks.clone(), }; // Generate the proofs. @@ -392,6 +399,11 @@ impl ProofBuilder for StateChainProofBuilder { self.client .verify(&proof, &self.verifying_key) .context("Failed to verify generated state chain proof")?; + anyhow::ensure!( + proof.zkm_version == ZKM_CIRCUIT_VERSION, + "generated state-chain proof has unexpected Ziren version {}", + proof.zkm_version + ); let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) diff --git a/circuits/state-chain-proof/host/src/main.rs b/circuits/state-chain-proof/host/src/main.rs index 21b16511d..37069fcd8 100644 --- a/circuits/state-chain-proof/host/src/main.rs +++ b/circuits/state-chain-proof/host/src/main.rs @@ -11,6 +11,12 @@ async fn main() { tracing::info!("args: {:?}", args); // Setup the logger. zkm_sdk::utils::setup_logger(); + let builder = StateChainProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let blocks = fetch_state_chain( &args.l2_contract_addresses, &args.proceed_withdraw_method_ids, @@ -24,8 +30,6 @@ async fn main() { .await .unwrap(); - let builder = StateChainProofBuilder::new(); - let ctx = ProofRequest::StateChainProofRequest { init_input: args.init_input, input_proof: args.input_proof.clone(), diff --git a/circuits/watchtower-proof/guest/Cargo.lock b/circuits/watchtower-proof/guest/Cargo.lock index b889d4717..573afc9fe 100644 --- a/circuits/watchtower-proof/guest/Cargo.lock +++ b/circuits/watchtower-proof/guest/Cargo.lock @@ -2428,6 +2428,7 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", @@ -5760,6 +5761,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] diff --git a/circuits/watchtower-proof/host/Cargo.toml b/circuits/watchtower-proof/host/Cargo.toml index 427c24b7a..84306b5b5 100644 --- a/circuits/watchtower-proof/host/Cargo.toml +++ b/circuits/watchtower-proof/host/Cargo.toml @@ -31,6 +31,7 @@ commit-chain = { workspace = true } bitcoin = { workspace = true } client = { workspace = true } proof-builder.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/watchtower-proof/host/src/lib.rs b/circuits/watchtower-proof/host/src/lib.rs index d488cc8a2..3a1606b86 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -5,8 +5,8 @@ use borsh::BorshDeserialize; use commit_chain::{CommitChainCircuitInput, CommitChainPrevProofType}; use header_chain::{CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType}; use zkm_sdk::{ - HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, - include_elf, + HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofKind, ZKMProofWithPublicValues, + ZKMStdin, include_elf, }; use bitcoin::{Block, Network, Transaction, Txid, hashes::Hash}; @@ -25,6 +25,10 @@ use std::fs; // The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -40,7 +44,7 @@ pub struct Args { #[clap(long, env)] pub latest_sequencer_commit_txid: String, - #[clap(long, env, short)] + #[clap(long, env, short = 'H')] pub header_chain_input_proof: String, #[clap(long, env, short)] @@ -94,6 +98,11 @@ impl WatchtowerProofBuilder { let (proving_key, verifying_key) = client.setup(WATCHTOWER); Self { client, proving_key, verifying_key } } + + pub fn program_id(&self) -> anyhow::Result { + verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } } impl ProofBuilder for WatchtowerProofBuilder { @@ -149,6 +158,8 @@ impl ProofBuilder for WatchtowerProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; HeaderChainCircuitInput { prev_proof: HeaderChainPrevProofType::GenesisBlock, // unused @@ -156,6 +167,7 @@ impl ProofBuilder for WatchtowerProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers: vec![], } }; @@ -177,12 +189,15 @@ impl ProofBuilder for WatchtowerProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; CommitChainCircuitInput { prev_proof: CommitChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, commits: vec![], } }; @@ -203,12 +218,15 @@ impl ProofBuilder for WatchtowerProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; StateChainCircuitInput { prev_proof: StateChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: vec![], } }; diff --git a/circuits/watchtower-proof/host/src/main.rs b/circuits/watchtower-proof/host/src/main.rs index d5d2a8186..a39701d03 100644 --- a/circuits/watchtower-proof/host/src/main.rs +++ b/circuits/watchtower-proof/host/src/main.rs @@ -10,6 +10,12 @@ async fn main() { let args = Args::parse(); // Setup the logger. zkm_sdk::utils::setup_logger(); + let builder = WatchtowerProofBuilder::new(); + if args.print_program_id { + println!("{}", hex::encode(builder.program_id().unwrap())); + return; + } + let (block_pos, target_block, latest_sequencer_commit_tx) = fetch_target_block( &args.esplora_url, &args.latest_sequencer_commit_txid, @@ -17,8 +23,6 @@ async fn main() { ) .await .unwrap(); - let builder = WatchtowerProofBuilder::new(); - let ctx = ProofRequest::WatchtowerProofRequest { genesis_sequencer_commit_txid: args.genesis_sequencer_commit_txid.clone(), latest_sequencer_commit_txid: args.latest_sequencer_commit_txid.clone(), diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index 8dc6b808c..b52a98941 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -26,6 +26,20 @@ pub use guest_executor::io::EthClientExecutorInput; use serde::{Deserialize, Serialize}; use verifier::verify_groth16_proof; +// Regenerate these IDs after changing a leaf guest or the verifier. +// TODO: Generated by `proof-builder-rpc` +pub const EXPECTED_HEADER_CHAIN_PROGRAM_ID: verifier::ProgramId = [ + 0x87, 0x6b, 0xd8, 0x4c, 0xc8, 0x9b, 0xa4, 0x57, 0xa0, 0xfb, 0x94, 0xfc, 0x20, 0x13, 0xbd, 0x75, + 0x66, 0xae, 0xcc, 0x1e, 0x04, 0xbe, 0xfa, 0x53, 0x6c, 0x0c, 0x38, 0xd9, 0x57, 0x48, 0xf4, 0xf4, +]; +pub const EXPECTED_STATE_CHAIN_PROGRAM_ID: verifier::ProgramId = [ + 0x73, 0xc0, 0x74, 0x70, 0xf9, 0x7b, 0x3f, 0x8d, 0xe7, 0x48, 0x94, 0xad, 0x1a, 0xec, 0x60, 0xfb, + 0x82, 0x2c, 0x67, 0xf2, 0x97, 0x3a, 0x6f, 0xda, 0xef, 0x9f, 0x2b, 0xa6, 0xab, 0xb7, 0x3a, 0x17, +]; +pub const EXPECTED_COMMIT_CHAIN_PROGRAM_ID: verifier::ProgramId = [ + 0x93, 0x91, 0xe0, 0x33, 0x40, 0x2b, 0x86, 0x2b, 0x5e, 0x74, 0xf7, 0xd6, 0x65, 0xac, 0x91, 0xcf, + 0x80, 0xc2, 0xb2, 0x6f, 0xb1, 0x0f, 0x0c, 0x72, 0xcb, 0x92, 0xc9, 0x64, 0x17, 0xc1, 0x12, 0xf6, +]; pub const GRAPH_ID_SIZE: usize = 16; pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; @@ -48,6 +62,19 @@ pub struct OperatorPublicOutputs { pub included_watchtowers: [u8; 32], } +fn checked_history_root( + program_type: verifier::ProgramType, + actual_program_id: verifier::ProgramId, + output_program_id: verifier::ProgramId, + expected_program_id: verifier::ProgramId, + history: [u8; 32], +) -> [u8; 32] { + assert_eq!(actual_program_id, expected_program_id, "unexpected proof program id"); + assert_eq!(output_program_id, actual_program_id, "proof output program id mismatch"); + + verifier::finalize_history(program_type, history, actual_program_id) +} + pub fn decode_operator_public_outputs( public_values: &[u8], ) -> Result { @@ -73,7 +100,7 @@ pub fn watch_longest_chain( // * 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( + let commit_program_id = verify_groth16_proof( &commit_chain.zkm_proof, &commit_chain.zkm_public_values, &commit_chain.zkm_vk_hash, @@ -90,7 +117,7 @@ pub fn watch_longest_chain( println!("header chain: applying: {}", header_chain.block_headers.len()); // verify header_chain is valid - verify_groth16_proof( + let header_program_id = verify_groth16_proof( &header_chain.zkm_proof, &header_chain.zkm_public_values, &header_chain.zkm_vk_hash, @@ -104,7 +131,7 @@ pub fn watch_longest_chain( println!("SPV"); assert!(spv.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); - verify_groth16_proof( + let state_program_id = verify_groth16_proof( &state_chain.zkm_proof, &state_chain.zkm_public_values, &state_chain.zkm_vk_hash, @@ -128,6 +155,31 @@ pub fn watch_longest_chain( let commitment = commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); let commitment = parse_commit_chain_commitment(&commitment); + let program_history_root = verifier::program_history_root( + checked_history_root( + verifier::ProgramType::Header, + header_program_id, + btc_header_chain_output.self_program_id, + EXPECTED_HEADER_CHAIN_PROGRAM_ID, + btc_header_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::State, + state_program_id, + state_chain_output.self_program_id, + EXPECTED_STATE_CHAIN_PROGRAM_ID, + state_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::Commit, + commit_program_id, + commit_chain_output.self_program_id, + EXPECTED_COMMIT_CHAIN_PROGRAM_ID, + commit_chain_output.program_history_hash, + ), + ); + assert_eq!(commitment.program_history_root, program_history_root); + if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { assert_eq!(commitment.sequencer_set_hash, x); } else { @@ -226,6 +278,7 @@ pub fn propose_longest_chain( watchtower_challenge_txn_scripts: Vec, watchtower_challenge_txn_prev_outs: Vec, graph_watchtower_xonly_public_keys: &[[u8; 32]], + expected_watchtower_program_id: verifier::ProgramId, operator_header_chain: HeaderChainCircuitInput, commit_chain: CommitChainCircuitInput, @@ -235,7 +288,7 @@ pub fn propose_longest_chain( ) -> ([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( + let commit_program_id = verify_groth16_proof( &commit_chain.zkm_proof, &commit_chain.zkm_public_values, &commit_chain.zkm_vk_hash, @@ -254,7 +307,7 @@ 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 - verify_groth16_proof( + let header_program_id = verify_groth16_proof( &operator_header_chain.zkm_proof, &operator_header_chain.zkm_public_values, &operator_header_chain.zkm_vk_hash, @@ -357,7 +410,11 @@ pub fn propose_longest_chain( }; match verify_groth16_proof(&proof, &public_values, &vk, &zkm_version) { - Ok(_) => {} + Ok(program_id) if program_id == expected_watchtower_program_id => {} + Ok(_) => { + println!("Watchtower[{i}] unexpected program id"); + continue; + } Err(err) => { println!("Watchtower[{i}] invalid proof: {err}"); continue; @@ -401,7 +458,7 @@ pub fn propose_longest_chain( } println!("verify el block"); - verify_groth16_proof( + let state_program_id = verify_groth16_proof( &state_chain.zkm_proof, &state_chain.zkm_public_values, &state_chain.zkm_vk_hash, @@ -425,6 +482,31 @@ pub fn propose_longest_chain( let commitment = commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); let commitment = parse_commit_chain_commitment(&commitment); + let program_history_root = verifier::program_history_root( + checked_history_root( + verifier::ProgramType::Header, + header_program_id, + btc_header_chain_output.self_program_id, + EXPECTED_HEADER_CHAIN_PROGRAM_ID, + btc_header_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::State, + state_program_id, + state_chain_output.self_program_id, + EXPECTED_STATE_CHAIN_PROGRAM_ID, + state_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::Commit, + commit_program_id, + commit_chain_output.self_program_id, + EXPECTED_COMMIT_CHAIN_PROGRAM_ID, + commit_chain_output.program_history_hash, + ), + ); + assert_eq!(commitment.program_history_root, program_history_root); + if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { assert_eq!(commitment.sequencer_set_hash, x); } else { @@ -673,6 +755,7 @@ pub fn parse_watchtower_commitment( #[cfg(test)] mod tests { use super::*; + use bitcoin::Transaction; const PROOF: &[u8] = include_bytes!("../../../circuits/data/watchtower/output3.bin.proof.bin"); const PUBLIC_INPUTS: &[u8] = @@ -680,6 +763,44 @@ 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 checked_history_root_binds_actual_output_and_expected_program_ids() { + let program_id = [1u8; 32]; + let history = [2u8; 32]; + assert_eq!( + checked_history_root( + verifier::ProgramType::Header, + program_id, + program_id, + program_id, + history, + ), + verifier::finalize_history(verifier::ProgramType::Header, history, program_id) + ); + + let wrong_expected = std::panic::catch_unwind(|| { + checked_history_root( + verifier::ProgramType::Header, + program_id, + program_id, + [3u8; 32], + history, + ) + }); + assert!(wrong_expected.is_err()); + + let wrong_output = std::panic::catch_unwind(|| { + checked_history_root( + verifier::ProgramType::Header, + program_id, + [3u8; 32], + program_id, + history, + ) + }); + assert!(wrong_output.is_err()); + } + #[test] fn test_build_watchtower_commitment() { let graph_id = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); diff --git a/crates/bitcoin-light-client-circuit/src/utils.rs b/crates/bitcoin-light-client-circuit/src/utils.rs index e796e6ed2..1ec489a00 100644 --- a/crates/bitcoin-light-client-circuit/src/utils.rs +++ b/crates/bitcoin-light-client-circuit/src/utils.rs @@ -82,7 +82,7 @@ pub fn create_fee_tx( } pub fn create_sequencer_update_partial_tx( - commitment: [u8; 96], + commitment: [u8; 128], update_connector: &Option, replenish_fee_connector: &Option, next_update_connector: Address, diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index 212245acc..fa512cda0 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -30,6 +30,7 @@ pub struct CommitInfo { pub enum CommitChainPrevProofType { GenesisBlock, PrevProof, + LegacyPrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -117,6 +118,7 @@ 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 EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE: usize = 128; pub const LEGACY_OPERATOR_VK_HASH: [u8; 32] = [0u8; 32]; #[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] @@ -124,11 +126,19 @@ pub struct CommitChainCommitment { pub sequencer_set_hash: [u8; 32], pub genesis_evm_block_hash: [u8; 32], pub operator_vk_hash: [u8; 32], + pub program_history_root: [u8; 32], } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct CommitChainCircuitOutput { pub chain_state: CommitChainState, + pub self_program_id: verifier::ProgramId, + pub program_history_hash: [u8; 32], +} + +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +struct PreIdentityCommitChainCircuitOutput { + chain_state: CommitChainState, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -159,6 +169,8 @@ impl From for CommitChainCircuitOutput { threshold: chain_state.threshold, operator_vk_hash: LEGACY_OPERATOR_VK_HASH, }, + self_program_id: [0u8; 32], + program_history_hash: [0u8; 32], } } } @@ -170,9 +182,30 @@ pub struct CommitChainCircuitInput { pub zkm_public_values: Vec, pub zkm_vk_hash: Vec, pub zkm_version: String, + pub self_program_id: verifier::ProgramId, pub commits: Vec, } +pub fn classify_commit_chain_output( + public_values: &[u8], +) -> Result { + if bincode::deserialize::(public_values).is_ok() { + return Ok(CommitChainPrevProofType::PrevProof); + } + if bincode::deserialize::(public_values).is_ok() { + return Ok(CommitChainPrevProofType::LegacyPrevProof); + } + Err("unknown commit-chain public output format".to_string()) +} + +pub fn decode_pre_identity_commit_chain_output( + public_values: &[u8], +) -> Result { + bincode::deserialize::(public_values) + .map(|output| output.chain_state) + .map_err(|err| format!("invalid pre-identity commit-chain output: {err}")) +} + pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> Hash { let sequencer_set = ValidatorSet::without_proposer(sequencers.iter().cloned().map(|s| s.into()).collect()); @@ -182,8 +215,9 @@ pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> 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" + || commitment.len() == COMMIT_CHAIN_COMMITMENT_SIZE + || commitment.len() == EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE, + "commit chain commitment must be 64, 96, or 128 bytes" ); let mut sequencer_set_hash = [0u8; 32]; @@ -191,15 +225,28 @@ pub fn parse_commit_chain_commitment(commitment: &[u8]) -> CommitChainCommitment 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..]); + if commitment.len() >= COMMIT_CHAIN_COMMITMENT_SIZE { + operator_vk_hash.copy_from_slice(&commitment[64..96]); assert_ne!( operator_vk_hash, LEGACY_OPERATOR_VK_HASH, "new commit chain commitment must include non-zero operator vk hash" ); } + let mut program_history_root = [0u8; 32]; + if commitment.len() == EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE { + program_history_root.copy_from_slice(&commitment[96..128]); + assert_ne!( + program_history_root, [0u8; 32], + "extended commit chain commitment must include non-zero program history root" + ); + } - CommitChainCommitment { sequencer_set_hash, genesis_evm_block_hash, operator_vk_hash } + CommitChainCommitment { + sequencer_set_hash, + genesis_evm_block_hash, + operator_vk_hash, + program_history_root, + } } /// Decode current or legacy commit-chain public values. @@ -208,6 +255,14 @@ pub fn decode_commit_chain_circuit_output(public_values: &[u8]) -> CommitChainCi return output; } + if let Ok(output) = bincode::deserialize::(public_values) { + return CommitChainCircuitOutput { + chain_state: output.chain_state, + self_program_id: [0u8; 32], + program_history_hash: [0u8; 32], + }; + } + bincode::deserialize::(public_values) .map(Into::into) .expect("failed to decode commit chain circuit output as current or legacy format") @@ -415,6 +470,27 @@ mod tests { 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); + assert_eq!(commitment.program_history_root, [0u8; 32]); + } + + #[test] + fn test_parse_commit_chain_commitment_splits_128_byte_payload() { + let sequencer_set_hash = [0x11u8; 32]; + let genesis_evm_block_hash = [0x22u8; 32]; + let operator_vk_hash = [0x33u8; 32]; + let program_history_root = [0x44u8; 32]; + let mut payload = Vec::with_capacity(128); + payload.extend_from_slice(&sequencer_set_hash); + payload.extend_from_slice(&genesis_evm_block_hash); + payload.extend_from_slice(&operator_vk_hash); + payload.extend_from_slice(&program_history_root); + + 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); + assert_eq!(commitment.program_history_root, program_history_root); } #[test] @@ -430,6 +506,7 @@ mod tests { 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); + assert_eq!(commitment.program_history_root, [0u8; 32]); } #[test] @@ -442,6 +519,16 @@ mod tests { assert!(result.is_err()); } + #[test] + fn test_parse_commit_chain_commitment_rejects_zero_program_history_root() { + let mut payload = vec![0x11u8; EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE]; + payload[96..].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, diff --git a/crates/commit-chain/src/lib.rs b/crates/commit-chain/src/lib.rs index e17369129..9b0b4f365 100644 --- a/crates/commit-chain/src/lib.rs +++ b/crates/commit-chain/src/lib.rs @@ -4,13 +4,15 @@ mod commit_chain; pub use commit_chain::*; pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircuitOutput { - let mut chain_state = match input.prev_proof { - CommitChainPrevProofType::GenesisBlock => { - CommitChainState::new(input.commits[0].genesis_txid) - } + let self_program_id = input.self_program_id; + let (mut chain_state, program_history_hash) = match input.prev_proof { + CommitChainPrevProofType::GenesisBlock => ( + CommitChainState::new(input.commits[0].genesis_txid), + verifier::initial_history(verifier::ProgramType::Commit), + ), CommitChainPrevProofType::PrevProof => { println!("verify commit chain of prev proof"); - verifier::verify_groth16_proof( + let previous_program_id = verifier::verify_groth16_proof( &input.zkm_proof, &input.zkm_public_values, &input.zkm_vk_hash, @@ -18,10 +20,72 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui ) .unwrap(); - decode_commit_chain_circuit_output(&input.zkm_public_values).chain_state + let output: CommitChainCircuitOutput = + bincode::deserialize(&input.zkm_public_values).unwrap(); + assert_eq!(output.self_program_id, previous_program_id); + let history = verifier::next_history( + verifier::ProgramType::Commit, + output.program_history_hash, + previous_program_id, + self_program_id, + ); + (output.chain_state, history) + } + CommitChainPrevProofType::LegacyPrevProof => { + let previous_program_id = verifier::verify_groth16_proof( + &input.zkm_proof, + &input.zkm_public_values, + &input.zkm_vk_hash, + &input.zkm_version, + ) + .unwrap(); + let chain_state = + decode_pre_identity_commit_chain_output(&input.zkm_public_values).unwrap(); + let history = verifier::legacy_history( + verifier::ProgramType::Commit, + previous_program_id, + &input.zkm_public_values, + ); + (chain_state, history) } }; chain_state.apply_commit(input.commits); - CommitChainCircuitOutput { chain_state } + CommitChainCircuitOutput { chain_state, self_program_id, program_history_hash } +} + +#[cfg(test)] +mod circuit_output_tests { + use super::*; + use serde::Serialize; + + #[derive(Serialize)] + struct LegacyOutput { + chain_state: CommitChainState, + } + + fn chain_state() -> CommitChainState { + CommitChainState::new([1u8; 32]) + } + + #[test] + fn classifies_only_current_and_immediate_legacy_outputs() { + let legacy = bincode::serialize(&LegacyOutput { chain_state: chain_state() }).unwrap(); + assert_eq!( + classify_commit_chain_output(&legacy).unwrap(), + CommitChainPrevProofType::LegacyPrevProof + ); + + let current = bincode::serialize(&CommitChainCircuitOutput { + chain_state: chain_state(), + self_program_id: [1u8; 32], + program_history_hash: [2u8; 32], + }) + .unwrap(); + assert_eq!( + classify_commit_chain_output(¤t).unwrap(), + CommitChainPrevProofType::PrevProof + ); + assert!(classify_commit_chain_output(b"unknown").is_err()); + } } diff --git a/crates/header-chain/Cargo.toml b/crates/header-chain/Cargo.toml index 12f18cec8..603ac692a 100644 --- a/crates/header-chain/Cargo.toml +++ b/crates/header-chain/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true #resolver.workspace = true [dependencies] +bincode = "1.3.3" borsh = { version = "1.5.3", features = ["derive"] } sha2 = { version = "0.10.9", default-features = false } bitcoin = { workspace = true, features = ["serde"] } diff --git a/crates/header-chain/src/header_chain.rs b/crates/header-chain/src/header_chain.rs index 78ea24b06..e3b081162 100644 --- a/crates/header-chain/src/header_chain.rs +++ b/crates/header-chain/src/header_chain.rs @@ -363,6 +363,13 @@ fn calculate_work(target: &[u8; 32]) -> U256 { )] pub struct BlockHeaderCircuitOutput { pub chain_state: ChainState, + pub self_program_id: verifier::ProgramId, + pub program_history_hash: [u8; 32], +} + +#[derive(Deserialize)] +struct LegacyBlockHeaderCircuitOutput { + chain_state: ChainState, } /// The input proof of the header chain circuit. @@ -371,6 +378,25 @@ pub struct BlockHeaderCircuitOutput { pub enum HeaderChainPrevProofType { GenesisBlock, PrevProof, + LegacyPrevProof, +} + +pub fn classify_header_chain_output( + public_values: &[u8], +) -> Result { + if bincode::deserialize::(public_values).is_ok() { + return Ok(HeaderChainPrevProofType::PrevProof); + } + if bincode::deserialize::(public_values).is_ok() { + return Ok(HeaderChainPrevProofType::LegacyPrevProof); + } + Err("unknown header-chain public output format".to_string()) +} + +pub fn decode_legacy_header_chain_output(public_values: &[u8]) -> Result { + bincode::deserialize::(public_values) + .map(|output| output.chain_state) + .map_err(|err| format!("invalid legacy header-chain output: {err}")) } /// The input of the header chain circuit. @@ -380,6 +406,7 @@ pub struct HeaderChainCircuitInput { pub zkm_public_values: Vec, pub zkm_vk_hash: Vec, pub zkm_version: String, + pub self_program_id: verifier::ProgramId, pub prev_proof: HeaderChainPrevProofType, pub block_headers: Vec, } diff --git a/crates/header-chain/src/lib.rs b/crates/header-chain/src/lib.rs index 142335f06..bf6234204 100644 --- a/crates/header-chain/src/lib.rs +++ b/crates/header-chain/src/lib.rs @@ -10,8 +10,6 @@ pub use merkle_tree::*; pub use mmr::*; pub use transaction::*; -use zkm_primitives::io::ZKMPublicValues; - pub mod spv; pub use spv::SPV; @@ -19,11 +17,14 @@ pub use spv::SPV; pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircuitOutput { // println!("Detected network: {:?}", NETWORK_TYPE); // println!("NETWORK_CONSTANTS: {:?}", NETWORK_CONSTANTS); - let mut chain_state = match input.prev_proof { - HeaderChainPrevProofType::GenesisBlock => ChainState::new(), + let self_program_id = input.self_program_id; + let (mut chain_state, program_history_hash) = match input.prev_proof { + HeaderChainPrevProofType::GenesisBlock => { + (ChainState::new(), verifier::initial_history(verifier::ProgramType::Header)) + } HeaderChainPrevProofType::PrevProof => { println!("verify header chain of prev proof"); - verifier::verify_groth16_proof( + let previous_program_id = verifier::verify_groth16_proof( &input.zkm_proof, &input.zkm_public_values, &input.zkm_vk_hash, @@ -31,12 +32,67 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui ) .unwrap(); - let btc_header_chain_output: BlockHeaderCircuitOutput = - ZKMPublicValues::from(&input.zkm_public_values).read(); - btc_header_chain_output.chain_state + let output: BlockHeaderCircuitOutput = + bincode::deserialize(&input.zkm_public_values).unwrap(); + assert_eq!(output.self_program_id, previous_program_id); + let history = verifier::next_history( + verifier::ProgramType::Header, + output.program_history_hash, + previous_program_id, + self_program_id, + ); + (output.chain_state, history) + } + HeaderChainPrevProofType::LegacyPrevProof => { + let previous_program_id = verifier::verify_groth16_proof( + &input.zkm_proof, + &input.zkm_public_values, + &input.zkm_vk_hash, + &input.zkm_version, + ) + .unwrap(); + let chain_state = decode_legacy_header_chain_output(&input.zkm_public_values).unwrap(); + let history = verifier::legacy_history( + verifier::ProgramType::Header, + previous_program_id, + &input.zkm_public_values, + ); + (chain_state, history) } }; chain_state.apply_blocks(input.block_headers); - BlockHeaderCircuitOutput { chain_state } + BlockHeaderCircuitOutput { chain_state, self_program_id, program_history_hash } +} + +#[cfg(test)] +mod circuit_output_tests { + use super::*; + use serde::Serialize; + + #[derive(Serialize)] + struct LegacyOutput { + chain_state: ChainState, + } + + #[test] + fn classifies_only_current_and_immediate_legacy_outputs() { + let legacy = bincode::serialize(&LegacyOutput { chain_state: ChainState::new() }).unwrap(); + assert_eq!( + classify_header_chain_output(&legacy).unwrap(), + HeaderChainPrevProofType::LegacyPrevProof + ); + + let current = bincode::serialize(&BlockHeaderCircuitOutput { + chain_state: ChainState::new(), + self_program_id: [1u8; 32], + program_history_hash: [2u8; 32], + }) + .unwrap(); + assert_eq!( + classify_header_chain_output(¤t).unwrap(), + HeaderChainPrevProofType::PrevProof + ); + assert!(classify_header_chain_output(b"unknown").is_err()); + } } diff --git a/crates/state-chain/src/lib.rs b/crates/state-chain/src/lib.rs index 1a55b0b7a..aa14ac054 100644 --- a/crates/state-chain/src/lib.rs +++ b/crates/state-chain/src/lib.rs @@ -4,19 +4,21 @@ mod state_chain; pub use cbft::*; pub use state_chain::*; -use zkm_primitives::io::ZKMPublicValues; - pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOutput { - let mut chain_state = match input.prev_proof { + let self_program_id = input.self_program_id; + let (mut chain_state, program_history_hash) = match input.prev_proof { StateChainPrevProofType::GenesisBlock => { let block_hash: [u8; 32] = input.blocks[0].evm_block.current_block.hash_slow().into(); let block_height = input.blocks[0].evm_block.current_block.header.number; let cosmos_block = input.blocks[0].cosmos_block.clone(); - StateChainState::new(block_height, block_hash, cosmos_block) + ( + StateChainState::new(block_height, block_hash, cosmos_block), + verifier::initial_history(verifier::ProgramType::State), + ) } StateChainPrevProofType::PrevProof => { println!("verify state chain of prev proof"); - verifier::verify_groth16_proof( + let previous_program_id = verifier::verify_groth16_proof( &input.zkm_proof, &input.zkm_public_values, &input.zkm_vk_hash, @@ -25,11 +27,70 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu .unwrap(); let state_chain_output: StateChainCircuitOutput = - ZKMPublicValues::from(&input.zkm_public_values).read(); - state_chain_output.chain_state + bincode::deserialize(&input.zkm_public_values).unwrap(); + assert_eq!(state_chain_output.self_program_id, previous_program_id); + let history = verifier::next_history( + verifier::ProgramType::State, + state_chain_output.program_history_hash, + previous_program_id, + self_program_id, + ); + (state_chain_output.chain_state, history) + } + StateChainPrevProofType::LegacyPrevProof => { + let previous_program_id = verifier::verify_groth16_proof( + &input.zkm_proof, + &input.zkm_public_values, + &input.zkm_vk_hash, + &input.zkm_version, + ) + .unwrap(); + let chain_state = decode_legacy_state_chain_output(&input.zkm_public_values).unwrap(); + let history = verifier::legacy_history( + verifier::ProgramType::State, + previous_program_id, + &input.zkm_public_values, + ); + (chain_state, history) } }; chain_state.apply_blocks(input.blocks); - StateChainCircuitOutput { chain_state } + StateChainCircuitOutput { chain_state, self_program_id, program_history_hash } +} + +#[cfg(test)] +mod circuit_output_tests { + use super::*; + use serde::Serialize; + + #[derive(Serialize)] + struct LegacyOutput { + chain_state: StateChainState, + } + + fn chain_state() -> StateChainState { + StateChainState::new(1, [1u8; 32], Vec::new()) + } + + #[test] + fn classifies_only_current_and_immediate_legacy_outputs() { + let legacy = bincode::serialize(&LegacyOutput { chain_state: chain_state() }).unwrap(); + assert_eq!( + classify_state_chain_output(&legacy).unwrap(), + StateChainPrevProofType::LegacyPrevProof + ); + + let current = bincode::serialize(&StateChainCircuitOutput { + chain_state: chain_state(), + self_program_id: [1u8; 32], + program_history_hash: [2u8; 32], + }) + .unwrap(); + assert_eq!( + classify_state_chain_output(¤t).unwrap(), + StateChainPrevProofType::PrevProof + ); + assert!(classify_state_chain_output(b"unknown").is_err()); + } } diff --git a/crates/state-chain/src/state_chain.rs b/crates/state-chain/src/state_chain.rs index 3c9f3fe38..c99a19212 100644 --- a/crates/state-chain/src/state_chain.rs +++ b/crates/state-chain/src/state_chain.rs @@ -17,6 +17,7 @@ type WithdrawalSlot = (Address, [u8; 32], Vec<[u8; 16]>); pub enum StateChainPrevProofType { GenesisBlock, PrevProof, + LegacyPrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -42,6 +43,31 @@ pub struct StateChainState { #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct StateChainCircuitOutput { pub chain_state: StateChainState, + pub self_program_id: verifier::ProgramId, + pub program_history_hash: [u8; 32], +} + +#[derive(Deserialize)] +struct LegacyStateChainCircuitOutput { + chain_state: StateChainState, +} + +pub fn classify_state_chain_output( + public_values: &[u8], +) -> Result { + if bincode::deserialize::(public_values).is_ok() { + return Ok(StateChainPrevProofType::PrevProof); + } + if bincode::deserialize::(public_values).is_ok() { + return Ok(StateChainPrevProofType::LegacyPrevProof); + } + Err("unknown state-chain public output format".to_string()) +} + +pub fn decode_legacy_state_chain_output(public_values: &[u8]) -> Result { + bincode::deserialize::(public_values) + .map(|output| output.chain_state) + .map_err(|err| format!("invalid legacy state-chain output: {err}")) } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -50,6 +76,7 @@ pub struct StateChainCircuitInput { pub zkm_public_values: Vec, pub zkm_vk_hash: Vec, pub zkm_version: String, + pub self_program_id: verifier::ProgramId, pub prev_proof: StateChainPrevProofType, pub blocks: Vec, } diff --git a/crates/verifier/Cargo.toml b/crates/verifier/Cargo.toml index 9201485e8..d9deba1be 100644 --- a/crates/verifier/Cargo.toml +++ b/crates/verifier/Cargo.toml @@ -4,7 +4,8 @@ version.workspace = true edition.workspace = true [dependencies] +sha2 = { workspace = true } zkm-verifier = { workspace = true } [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/crates/verifier/src/lib.rs b/crates/verifier/src/lib.rs index e08fde4c2..b024052fa 100644 --- a/crates/verifier/src/lib.rs +++ b/crates/verifier/src/lib.rs @@ -1,23 +1,104 @@ -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; +use sha2::{Digest, Sha256}; +use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES, decode_zkm_vkey_hash}; + +pub type ProgramId = [u8; 32]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProgramType { + Header = 1, + State = 2, + Commit = 3, +} + +fn tagged_hash(tag: &[u8], parts: &[&[u8]]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(tag); + for part in parts { + hasher.update(part); + } + hasher.finalize().into() +} + +fn program_id_with_part_vk(zkm_vk_hash: &str, part_stark_vk: &[u8]) -> Result { + let vk_hash = decode_zkm_vkey_hash(zkm_vk_hash).map_err(|e| format!("{e:?}"))?; + let part_vk_hash: [u8; 32] = Sha256::digest(part_stark_vk).into(); + Ok(tagged_hash(b"bitvm2/program-id/v1", &[&vk_hash, &part_vk_hash])) +} + +pub fn program_id(zkm_vk_hash: &[u8], zkm_version: &str) -> Result { + let zkm_vk_hash = String::from_utf8(zkm_vk_hash.to_vec()).map_err(|e| e.to_string())?; + program_id_with_part_vk(&zkm_vk_hash, Groth16Verifier::get_part_stark_vk(zkm_version)) +} + +pub fn initial_history(program_type: ProgramType) -> [u8; 32] { + tagged_hash(b"bitvm2/vk-history-seed/v1", &[&[program_type as u8]]) +} + +pub fn legacy_history( + program_type: ProgramType, + previous_program_id: ProgramId, + previous_public_values: &[u8], +) -> [u8; 32] { + let public_values_hash: [u8; 32] = Sha256::digest(previous_public_values).into(); + tagged_hash( + b"bitvm2/vk-history-migration/v1", + &[&[program_type as u8], &previous_program_id, &public_values_hash], + ) +} + +pub fn next_history( + program_type: ProgramType, + previous_history: [u8; 32], + previous_program_id: ProgramId, + current_program_id: ProgramId, +) -> [u8; 32] { + if previous_program_id == current_program_id { + previous_history + } else { + tagged_hash( + b"bitvm2/vk-history-step/v1", + &[&[program_type as u8], &previous_history, &previous_program_id], + ) + } +} + +pub fn finalize_history( + program_type: ProgramType, + history: [u8; 32], + current_program_id: ProgramId, +) -> [u8; 32] { + tagged_hash( + b"bitvm2/vk-history-final/v1", + &[&[program_type as u8], &history, ¤t_program_id], + ) +} + +pub fn program_history_root( + header_history: [u8; 32], + state_history: [u8; 32], + commit_history: [u8; 32], +) -> [u8; 32] { + tagged_hash(b"bitvm2/program-history/v1", &[&header_history, &state_history, &commit_history]) +} pub fn verify_groth16_proof( proof: &[u8], zkm_public_values: &[u8], zkm_vk_hash: &[u8], zkm_version: &str, -) -> Result<(), String> { +) -> Result { 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( + 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:?}")), - } + ) + .map_err(|err| format!("Verify Groth16 proof, err: {err:?}"))?; + + program_id_with_part_vk(&zkm_vk_hash, part_stark_vk) } diff --git a/node/Cargo.toml b/node/Cargo.toml index 38ad959ad..650a031b6 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -132,4 +132,7 @@ store = { workspace = true } client = { workspace = true } bitcoin-light-client-circuit = { workspace = true } commit-chain = { workspace = true } +header-chain = { workspace = true } +state-chain = { workspace = true } +verifier = { workspace = true } cbft-rpc = { workspace = true } diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index f715eddfb..0960a1d42 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -36,7 +36,14 @@ use bitcoin_light_client_circuit::{ /*create_dummy_publisher_keys,*/ create_fee_tx, create_sequencer_update_partial_tx, estimate_tx_vbytes, }; +use commit_chain::{ + CommitChainCircuitOutput, CommitChainPrevProofType, classify_commit_chain_output, +}; use commit_chain::{CommitInfo, create_sequencer_update_script, finalize, sign_raw}; +use header_chain::{ + BlockHeaderCircuitOutput, HeaderChainPrevProofType, classify_header_chain_output, +}; +use state_chain::{StateChainCircuitOutput, StateChainPrevProofType, classify_state_chain_output}; use tendermint::validator::Info; use reqwest::Url; @@ -209,6 +216,20 @@ async fn get_sequencer_set_hash_from_db( #[derive(Subcommand, Debug)] enum Commands { + DeriveProgramHistoryRoot { + #[arg(long)] + header_chain_input_proof: String, + #[arg(long)] + state_chain_input_proof: String, + #[arg(long)] + commit_chain_input_proof: String, + #[arg(long, value_parser = hex_parse::<32>)] + next_header_program_id: [u8; 32], + #[arg(long, value_parser = hex_parse::<32>)] + next_state_program_id: [u8; 32], + #[arg(long, value_parser = hex_parse::<32>)] + next_commit_program_id: [u8; 32], + }, Pubkey { #[arg(long, short, value_delimiter = ',')] btc_key_wifs: Vec, @@ -232,6 +253,8 @@ enum Commands { goat_genesis_block_hash: [u8; 32], #[arg(long, env = "OPERATOR_VK_HASH", value_parser = hex_parse::<32>)] operator_vk_hash: [u8; 32], + #[arg(long, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] + program_history_root: [u8; 32], }, PushSeq { #[arg(long, env = "OWNER_BTC_KEY_WIF")] @@ -248,6 +271,8 @@ enum Commands { goat_genesis_block_hash: [u8; 32], #[arg(long, env = "OPERATOR_VK_HASH", value_parser = hex_parse::<32>)] operator_vk_hash: [u8; 32], + #[arg(long, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] + program_history_root: [u8; 32], #[arg(long)] commit_info: String, }, @@ -274,6 +299,26 @@ async fn main() -> Result<(), Box> { dotenv().ok(); let _ = tracing_subscriber::fmt().with_env_filter(EnvFilter::from_default_env()).try_init(); let args = Args::parse(); + if let Commands::DeriveProgramHistoryRoot { + header_chain_input_proof, + state_chain_input_proof, + commit_chain_input_proof, + next_header_program_id, + next_state_program_id, + next_commit_program_id, + } = &args.command + { + let root = derive_program_history_root( + header_chain_input_proof, + state_chain_input_proof, + commit_chain_input_proof, + *next_header_program_id, + *next_state_program_id, + *next_commit_program_id, + )?; + println!("0x{}", hex::encode(root)); + return Ok(()); + } let (btc_client, goat_client) = init_clients(&args).await?; let output_file = &args.output_file; @@ -287,6 +332,7 @@ async fn main() -> Result<(), Box> { }; match args.command { + Commands::DeriveProgramHistoryRoot { .. } => unreachable!(), Commands::Pubkey { btc_key_wifs } => { // calculate compressed public key btc_key_wifs.iter().for_each(|btc_key_wif| { @@ -337,6 +383,7 @@ async fn main() -> Result<(), Box> { next_publisher_btc_pubkeys, goat_genesis_block_hash, operator_vk_hash, + program_history_root, } => { let (sequencer_set_hash, goat_block_number, cosmos_block_number) = get_sequencer_set_hash_from_db(&args.db_path, goat_block_number, false).await?; @@ -358,6 +405,7 @@ async fn main() -> Result<(), Box> { sequencer_set_hash, goat_genesis_block_hash, operator_vk_hash, + program_history_root, goat_block_number, ) .await @@ -370,6 +418,7 @@ async fn main() -> Result<(), Box> { init_genesis, goat_genesis_block_hash, operator_vk_hash, + program_history_root, commit_info, } => { println!("goat genesis block hash: {:#?}", hex::encode(goat_genesis_block_hash)); @@ -393,6 +442,7 @@ async fn main() -> Result<(), Box> { sequencer_set_hash, goat_genesis_block_hash, operator_vk_hash, + program_history_root, output_file, ) .await?; @@ -416,6 +466,119 @@ async fn main() -> Result<(), Box> { } } +fn load_verified_proof(path: &str) -> anyhow::Result<(Vec, verifier::ProgramId)> { + let proof = std::fs::read(path)?; + let public_values = std::fs::read(format!("{path}.public_inputs.bin"))?; + let vk_hash = std::fs::read(format!("{path}.vk_hash.bin"))?; + let version = String::from_utf8(std::fs::read(format!("{path}.zkm_version.bin"))?)?; + let program_id = verifier::verify_groth16_proof(&proof, &public_values, &vk_hash, &version) + .map_err(anyhow::Error::msg)?; + Ok((public_values, program_id)) +} + +fn next_header_history_root( + path: &str, + next_program_id: verifier::ProgramId, +) -> anyhow::Result<[u8; 32]> { + let (public_values, previous_program_id) = load_verified_proof(path)?; + let history = match classify_header_chain_output(&public_values).map_err(anyhow::Error::msg)? { + HeaderChainPrevProofType::PrevProof => { + let output: BlockHeaderCircuitOutput = bincode::deserialize(&public_values)?; + anyhow::ensure!( + output.self_program_id == previous_program_id, + "header ProgramId mismatch" + ); + verifier::next_history( + verifier::ProgramType::Header, + output.program_history_hash, + previous_program_id, + next_program_id, + ) + } + HeaderChainPrevProofType::LegacyPrevProof => verifier::legacy_history( + verifier::ProgramType::Header, + previous_program_id, + &public_values, + ), + HeaderChainPrevProofType::GenesisBlock => unreachable!(), + }; + Ok(verifier::finalize_history(verifier::ProgramType::Header, history, next_program_id)) +} + +fn next_state_history_root( + path: &str, + next_program_id: verifier::ProgramId, +) -> anyhow::Result<[u8; 32]> { + let (public_values, previous_program_id) = load_verified_proof(path)?; + let history = match classify_state_chain_output(&public_values).map_err(anyhow::Error::msg)? { + StateChainPrevProofType::PrevProof => { + let output: StateChainCircuitOutput = bincode::deserialize(&public_values)?; + anyhow::ensure!( + output.self_program_id == previous_program_id, + "state ProgramId mismatch" + ); + verifier::next_history( + verifier::ProgramType::State, + output.program_history_hash, + previous_program_id, + next_program_id, + ) + } + StateChainPrevProofType::LegacyPrevProof => verifier::legacy_history( + verifier::ProgramType::State, + previous_program_id, + &public_values, + ), + StateChainPrevProofType::GenesisBlock => unreachable!(), + }; + Ok(verifier::finalize_history(verifier::ProgramType::State, history, next_program_id)) +} + +fn next_commit_history_root( + path: &str, + next_program_id: verifier::ProgramId, +) -> anyhow::Result<[u8; 32]> { + let (public_values, previous_program_id) = load_verified_proof(path)?; + let history = match classify_commit_chain_output(&public_values).map_err(anyhow::Error::msg)? { + CommitChainPrevProofType::PrevProof => { + let output: CommitChainCircuitOutput = bincode::deserialize(&public_values)?; + anyhow::ensure!( + output.self_program_id == previous_program_id, + "commit ProgramId mismatch" + ); + verifier::next_history( + verifier::ProgramType::Commit, + output.program_history_hash, + previous_program_id, + next_program_id, + ) + } + CommitChainPrevProofType::LegacyPrevProof => verifier::legacy_history( + verifier::ProgramType::Commit, + previous_program_id, + &public_values, + ), + CommitChainPrevProofType::GenesisBlock => unreachable!(), + }; + Ok(verifier::finalize_history(verifier::ProgramType::Commit, history, next_program_id)) +} + +/// Verifies the predecessor proofs and derives the root expected from their next recursive steps. +fn derive_program_history_root( + header_path: &str, + state_path: &str, + commit_path: &str, + next_header_program_id: verifier::ProgramId, + next_state_program_id: verifier::ProgramId, + next_commit_program_id: verifier::ProgramId, +) -> anyhow::Result<[u8; 32]> { + Ok(verifier::program_history_root( + next_header_history_root(header_path, next_header_program_id)?, + next_state_history_root(state_path, next_state_program_id)?, + next_commit_history_root(commit_path, next_commit_program_id)?, + )) +} + async fn push_fee_tx( fee_tx: &mut Transaction, input_value: Amount, @@ -546,6 +709,7 @@ async fn action_push_sequencer_set_update( sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], operator_vk_hash: [u8; 32], + program_history_root: [u8; 32], output_file: &str, ) -> Result<(), Box> { let witnesses = goat_client.ss_get_sequencer_set_update_witness(goat_block_number).await?; @@ -606,10 +770,11 @@ async fn action_push_sequencer_set_update( }; // Skip construction of the genesis tx - let mut commitment = [0u8; 96]; + let mut commitment = [0u8; 128]; commitment[0..32].copy_from_slice(&sequencer_set_hash); commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..].copy_from_slice(&operator_vk_hash); + commitment[64..96].copy_from_slice(&operator_vk_hash); + commitment[96..128].copy_from_slice(&program_history_root); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, &update_connector, @@ -656,6 +821,7 @@ async fn action_sign_sequencer_set_update( sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], operator_vk_hash: [u8; 32], + program_history_root: [u8; 32], goat_block_number: u64, ) -> Result<(), Box> { let total = btc_public_keys.len(); @@ -672,10 +838,11 @@ 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; 96]; + let mut commitment = [0u8; 128]; commitment[0..32].copy_from_slice(&sequencer_set_hash); commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..].copy_from_slice(&operator_vk_hash); + commitment[64..96].copy_from_slice(&operator_vk_hash); + commitment[96..128].copy_from_slice(&program_history_root); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, diff --git a/node/src/handle.rs b/node/src/handle.rs index 3ed9c50be..750c5d2d7 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -1801,15 +1801,7 @@ async fn handle_soldering_proof_ready_operator( 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" + "read soldering proof payload from store, start processing" ); handle_soldering_proof_payload_operator(ctx, &soldering_proof_ready, &payload).await } From 9648d31535b1395b3fa3bd60f14e947f4c4af9df Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Mon, 20 Jul 2026 18:35:20 +0800 Subject: [PATCH 05/11] refactor(cli): make runtime inputs optional for print-only mode --- circuits/commit-chain-proof/host/src/lib.rs | 9 +++- circuits/state-chain-proof/host/src/lib.rs | 22 ++++++--- circuits/watchtower-proof/host/src/lib.rs | 52 ++++++++++++++++++--- 3 files changed, 69 insertions(+), 14 deletions(-) diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 8c6ab57c4..cc89103fd 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -36,7 +36,14 @@ pub struct Args { #[arg(long, env, default_value = "http://127.0.0.1:3002")] pub esplora_url: String, - #[arg(long, env)] + // Print-only mode skips runtime inputs but keeps them required otherwise. + #[arg( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub commit_info: String, #[arg(long, default_value = "commits.bin")] diff --git a/circuits/state-chain-proof/host/src/lib.rs b/circuits/state-chain-proof/host/src/lib.rs index 0daaa2557..d38f3ff22 100644 --- a/circuits/state-chain-proof/host/src/lib.rs +++ b/circuits/state-chain-proof/host/src/lib.rs @@ -70,11 +70,24 @@ pub struct Args { #[clap(long, env, default_value_t = 0)] pub start: u64, - #[clap(long, env)] + // Print-only mode skips runtime inputs but keeps them required otherwise. + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub l2_contract_addresses: String, // https://explorer.testnet3.goat.network/address/0x9F0A61ce47678F43A326dB9F8964C56a924cd3D0?tab=read_write_contract - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub proceed_withdraw_method_ids: String, } @@ -399,11 +412,6 @@ impl ProofBuilder for StateChainProofBuilder { self.client .verify(&proof, &self.verifying_key) .context("Failed to verify generated state chain proof")?; - anyhow::ensure!( - proof.zkm_version == ZKM_CIRCUIT_VERSION, - "generated state-chain proof has unexpected Ziren version {}", - proof.zkm_version - ); let input = bincode::serialize(&input)?; Ok((input, proof, cycles, proving_time)) diff --git a/circuits/watchtower-proof/host/src/lib.rs b/circuits/watchtower-proof/host/src/lib.rs index 3a1606b86..baf91f8de 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -38,22 +38,62 @@ pub struct Args { #[arg(long, env, default_value_t = Network::Regtest)] pub bitcoin_network: Network, - #[clap(long, env)] + // Print-only mode skips runtime inputs but keeps them required otherwise. + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub genesis_sequencer_commit_txid: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub latest_sequencer_commit_txid: String, - #[clap(long, env, short = 'H')] + #[clap( + long, + env, + short = 'H', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub header_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub commit_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub state_chain_input_proof: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub output: String, } From 721c950906398967a974487ea253e3c3408a12ab Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Thu, 23 Jul 2026 21:24:12 +0800 Subject: [PATCH 06/11] refactor(commit-chain): remove operator_vk_hash and simplify commitment structure --- circuits/operator-proof/guest/src/main.rs | 4 +- circuits/operator-proof/host/src/lib.rs | 113 +++++++- circuits/operator-proof/host/src/main.rs | 10 +- .../bitcoin-light-client-circuit/src/lib.rs | 4 +- .../bitcoin-light-client-circuit/src/utils.rs | 2 +- crates/commit-chain/src/commit_chain.rs | 242 +++++------------- crates/commit-chain/src/lib.rs | 34 +-- node/src/bin/sequencer-set-publish.rs | 25 +- node/ssp-ci.sh | 20 +- 9 files changed, 199 insertions(+), 255 deletions(-) diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index fd3559423..af42022f3 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -10,8 +10,8 @@ use std::str::FromStr; // Regenerate this ID after changing the Watchtower guest. const EXPECTED_WATCHTOWER_PROGRAM_ID: [u8; 32] = [ - 0x84, 0xd5, 0x54, 0x57, 0x78, 0x53, 0xb3, 0xad, 0x73, 0x36, 0xee, 0xd8, 0xbf, 0x0a, 0x53, 0x0b, - 0x12, 0x69, 0x1a, 0xa2, 0xe5, 0x2f, 0xd5, 0xe8, 0x49, 0xa2, 0x08, 0x2a, 0xcf, 0xdd, 0xe1, 0x4f, + 0x23, 0xf1, 0xb7, 0x2f, 0x13, 0x8b, 0xa8, 0x5c, 0xaf, 0x75, 0x83, 0xe6, 0x8c, 0xb4, 0x77, 0xec, + 0xe1, 0x2d, 0x22, 0x28, 0x5a, 0x2c, 0x51, 0x06, 0xdc, 0x63, 0xc4, 0x5c, 0x34, 0x53, 0x8c, 0x2e, ]; pub fn main() { diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 17bff9c09..9588a969b 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -25,6 +25,10 @@ use zkm_sdk::{ /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -34,40 +38,119 @@ pub struct Args { #[arg(long, env, default_value_t = Network::Regtest)] pub bitcoin_network: Network, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub included_watchtowers: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub graph_id: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub latest_sequencer_commit_txid: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub operator_committed_blockhash: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub genesis_sequencer_commit_txid: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'H', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub header_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'c', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub commit_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 's', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub state_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'e', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("0")) + )] pub execution_layer_block_number: u64, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 't', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_challenge_txids: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'w', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_public_keys: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'i', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_challenge_init_txid: String, #[clap(long, env, default_value = "commit-proof.bin")] @@ -267,6 +350,12 @@ impl OperatorProofBuilder { let (proving_key, verifying_key) = client.setup(OPERATOR); Self { client, proving_key, verifying_key } } + + /// Returns the Program ID derived from this builder's verifying key. + pub fn program_id(&self) -> anyhow::Result { + verifier::program_id(self.verifying_key.bytes32().as_bytes(), zkm_sdk::ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } } impl ProofBuilder for OperatorProofBuilder { diff --git a/circuits/operator-proof/host/src/main.rs b/circuits/operator-proof/host/src/main.rs index 9101b09e8..95a2ddc35 100644 --- a/circuits/operator-proof/host/src/main.rs +++ b/circuits/operator-proof/host/src/main.rs @@ -3,6 +3,7 @@ use clap::Parser; use operator_proof::{Args, OperatorProofBuilder, fetch_target_block_and_watchtower_tx}; use proof_builder::{ProofBuilder, ProofRequest}; use util::hex_parse; +use zkm_sdk::HashableKey; #[tokio::main] async fn main() { @@ -11,6 +12,13 @@ async fn main() { // Setup the logger. zkm_sdk::utils::setup_logger(); + let builder = OperatorProofBuilder::new(); + if args.print_program_id { + println!("OPERATOR_PROGRAM_ID={}", hex::encode(builder.program_id().unwrap())); + eprintln!("OPERATOR_VK_HASH={}", builder.vk().bytes32()); + return; + } + let ( block_pos_ss_commit, target_block_ss_commit, @@ -34,8 +42,6 @@ async fn main() { .await .unwrap(); - let builder = OperatorProofBuilder::new(); - let ctx = ProofRequest::OperatorProofRequest { included_watchtowers: args.included_watchtowers.clone(), graph_id: hex_parse::<16>(&args.graph_id).unwrap(), diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index b52a98941..bf38dd58b 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -37,8 +37,8 @@ pub const EXPECTED_STATE_CHAIN_PROGRAM_ID: verifier::ProgramId = [ 0x82, 0x2c, 0x67, 0xf2, 0x97, 0x3a, 0x6f, 0xda, 0xef, 0x9f, 0x2b, 0xa6, 0xab, 0xb7, 0x3a, 0x17, ]; pub const EXPECTED_COMMIT_CHAIN_PROGRAM_ID: verifier::ProgramId = [ - 0x93, 0x91, 0xe0, 0x33, 0x40, 0x2b, 0x86, 0x2b, 0x5e, 0x74, 0xf7, 0xd6, 0x65, 0xac, 0x91, 0xcf, - 0x80, 0xc2, 0xb2, 0x6f, 0xb1, 0x0f, 0x0c, 0x72, 0xcb, 0x92, 0xc9, 0x64, 0x17, 0xc1, 0x12, 0xf6, + 0x66, 0x40, 0xd5, 0x04, 0x68, 0x8f, 0x8f, 0xbb, 0x98, 0xbf, 0x42, 0x27, 0x9a, 0x50, 0x6c, 0x8a, + 0xe8, 0x0f, 0xd6, 0x67, 0xa0, 0x24, 0x6d, 0x43, 0xd1, 0xd7, 0x21, 0xce, 0xe6, 0xd9, 0x56, 0x41, ]; pub const GRAPH_ID_SIZE: usize = 16; pub const PROOF_SIZE: usize = 260; diff --git a/crates/bitcoin-light-client-circuit/src/utils.rs b/crates/bitcoin-light-client-circuit/src/utils.rs index 1ec489a00..e796e6ed2 100644 --- a/crates/bitcoin-light-client-circuit/src/utils.rs +++ b/crates/bitcoin-light-client-circuit/src/utils.rs @@ -82,7 +82,7 @@ pub fn create_fee_tx( } pub fn create_sequencer_update_partial_tx( - commitment: [u8; 128], + commitment: [u8; 96], update_connector: &Option, replenish_fee_connector: &Option, next_update_connector: Address, diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index fa512cda0..e0fa0d975 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -9,6 +9,7 @@ pub use tendermint_light_client_verifier::{ types::{Hash, ValidatorSet}, }; +use bincode::Options as BincodeOptions; use bitcoin::{Transaction, TxOut, Witness, hashes::Hash as _, secp256k1::PublicKey}; #[derive(Serialize, Deserialize, Debug, PartialEq)] @@ -30,7 +31,6 @@ pub struct CommitInfo { pub enum CommitChainPrevProofType { GenesisBlock, PrevProof, - LegacyPrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -100,7 +100,6 @@ pub struct CommitChainState { pub sequencers: Vec, pub publisher_public_keys: Vec, pub threshold: u16, - pub operator_vk_hash: [u8; 32], } impl CircuitCommit { @@ -116,16 +115,12 @@ 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 EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE: usize = 128; -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], pub program_history_root: [u8; 32], } @@ -136,45 +131,6 @@ pub struct CommitChainCircuitOutput { pub program_history_hash: [u8; 32], } -#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] -struct PreIdentityCommitChainCircuitOutput { - 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, - }, - self_program_id: [0u8; 32], - program_history_hash: [0u8; 32], - } - } -} - #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct CommitChainCircuitInput { pub prev_proof: CommitChainPrevProofType, @@ -189,21 +145,19 @@ pub struct CommitChainCircuitInput { pub fn classify_commit_chain_output( public_values: &[u8], ) -> Result { - if bincode::deserialize::(public_values).is_ok() { + if deserialize_commit_chain_output(public_values).is_ok() { return Ok(CommitChainPrevProofType::PrevProof); } - if bincode::deserialize::(public_values).is_ok() { - return Ok(CommitChainPrevProofType::LegacyPrevProof); - } Err("unknown commit-chain public output format".to_string()) } -pub fn decode_pre_identity_commit_chain_output( +fn deserialize_commit_chain_output( public_values: &[u8], -) -> Result { - bincode::deserialize::(public_values) - .map(|output| output.chain_state) - .map_err(|err| format!("invalid pre-identity commit-chain output: {err}")) +) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .reject_trailing_bytes() + .deserialize(public_values) } pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> Hash { @@ -213,59 +167,30 @@ pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> Hash { } pub fn parse_commit_chain_commitment(commitment: &[u8]) -> CommitChainCommitment { - assert!( - commitment.len() == LEGACY_COMMIT_CHAIN_COMMITMENT_SIZE - || commitment.len() == COMMIT_CHAIN_COMMITMENT_SIZE - || commitment.len() == EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE, - "commit chain commitment must be 64, 96, or 128 bytes" + assert_eq!( + commitment.len(), + COMMIT_CHAIN_COMMITMENT_SIZE, + "commit chain commitment must be 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..96]); - assert_ne!( - operator_vk_hash, LEGACY_OPERATOR_VK_HASH, - "new commit chain commitment must include non-zero operator vk hash" - ); - } let mut program_history_root = [0u8; 32]; - if commitment.len() == EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE { - program_history_root.copy_from_slice(&commitment[96..128]); - assert_ne!( - program_history_root, [0u8; 32], - "extended commit chain commitment must include non-zero program history root" - ); - } + program_history_root.copy_from_slice(&commitment[64..96]); + assert_ne!( + program_history_root, [0u8; 32], + "commit chain commitment must include non-zero program history root" + ); - CommitChainCommitment { - sequencer_set_hash, - genesis_evm_block_hash, - operator_vk_hash, - program_history_root, - } + CommitChainCommitment { sequencer_set_hash, genesis_evm_block_hash, program_history_root } } -/// Decode current or legacy commit-chain public values. +/// Decode current 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; - } - - if let Ok(output) = bincode::deserialize::(public_values) { - return CommitChainCircuitOutput { - chain_state: output.chain_state, - self_program_id: [0u8; 32], - program_history_hash: [0u8; 32], - }; - } - - bincode::deserialize::(public_values) - .map(Into::into) - .expect("failed to decode commit chain circuit output as current or legacy format") + deserialize_commit_chain_output(public_values) + .expect("failed to decode current commit chain circuit output") } impl CommitChainState { @@ -282,7 +207,6 @@ impl CommitChainState { sequencers: Vec::new(), publisher_public_keys: vec![], threshold: u16::MAX, - operator_vk_hash: [0u8; 32], } } @@ -366,7 +290,6 @@ 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; } } } @@ -440,7 +363,7 @@ mod tests { fn commitment_payload( sequencers: &[SequencerInfo], genesis_evm_block_hash: [u8; 32], - operator_vk_hash: [u8; 32], + program_history_root: [u8; 32], ) -> PushBytesBuf { let mut payload = Vec::with_capacity(96); if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = @@ -451,7 +374,7 @@ mod tests { panic!("expected sha256 sequencer hash"); }; payload.extend_from_slice(&genesis_evm_block_hash); - payload.extend_from_slice(&operator_vk_hash); + payload.extend_from_slice(&program_history_root); PushBytesBuf::try_from(payload).expect("commitment payload is pushable") } @@ -459,95 +382,59 @@ mod tests { 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 program_history_root = [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); - assert_eq!(commitment.program_history_root, [0u8; 32]); - } - - #[test] - fn test_parse_commit_chain_commitment_splits_128_byte_payload() { - let sequencer_set_hash = [0x11u8; 32]; - let genesis_evm_block_hash = [0x22u8; 32]; - let operator_vk_hash = [0x33u8; 32]; - let program_history_root = [0x44u8; 32]; - let mut payload = Vec::with_capacity(128); - payload.extend_from_slice(&sequencer_set_hash); - payload.extend_from_slice(&genesis_evm_block_hash); - payload.extend_from_slice(&operator_vk_hash); payload.extend_from_slice(&program_history_root); 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); assert_eq!(commitment.program_history_root, program_history_root); } #[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); - assert_eq!(commitment.program_history_root, [0u8; 32]); + fn test_parse_commit_chain_commitment_rejects_old_payload_sizes() { + for size in [64, 128] { + let payload = vec![0x11u8; size]; + let result = std::panic::catch_unwind(|| parse_commit_chain_commitment(&payload)); + assert!(result.is_err(), "{size}-byte payload must be rejected"); + } } #[test] - fn test_parse_commit_chain_commitment_rejects_new_payload_with_zero_operator_vk_hash() { + #[should_panic(expected = "commit chain commitment must include non-zero program history root")] + fn test_parse_commit_chain_commitment_rejects_zero_program_history_root() { 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()); - } - - #[test] - fn test_parse_commit_chain_commitment_rejects_zero_program_history_root() { - let mut payload = vec![0x11u8; EXTENDED_COMMIT_CHAIN_COMMITMENT_SIZE]; - payload[96..].fill(0); - - let result = std::panic::catch_unwind(|| parse_commit_chain_commitment(&payload)); - - assert!(result.is_err()); + parse_commit_chain_commitment(&payload); } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] - struct LegacyCommitChainState { + struct OldCommitChainState { block_height: u32, commit_txn: Transaction, genesis_txid: [u8; 32], sequencers: Vec, publisher_public_keys: Vec, threshold: u16, + operator_vk_hash: [u8; 32], } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] - struct LegacyCommitChainCircuitOutput { - chain_state: LegacyCommitChainState, + struct OldCommitChainCircuitOutput { + chain_state: OldCommitChainState, + self_program_id: verifier::ProgramId, + program_history_hash: [u8; 32], } #[test] - fn test_decode_commit_chain_circuit_output_accepts_legacy_public_values() { - let legacy_output = LegacyCommitChainCircuitOutput { - chain_state: LegacyCommitChainState { + fn test_classify_commit_chain_output_rejects_old_operator_vk_hash_schema() { + let old_output = OldCommitChainCircuitOutput { + chain_state: OldCommitChainState { block_height: 7, commit_txn: Transaction { version: Version::TWO, @@ -559,15 +446,19 @@ mod tests { sequencers: vec![], publisher_public_keys: vec![], threshold: 0, + operator_vk_hash: [0x33; 32], }, + self_program_id: [0x44; 32], + program_history_hash: [0x55; 32], }; - let public_values = bincode::serialize(&legacy_output).unwrap(); + let public_values = bincode::serialize(&old_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); + assert!(bincode::deserialize::(&public_values).is_ok()); + assert!(classify_commit_chain_output(&public_values).is_err()); + assert!( + std::panic::catch_unwind(|| decode_commit_chain_circuit_output(&public_values)) + .is_err() + ); } // todo: use new commit file @@ -611,11 +502,11 @@ mod tests { let final_threshold = 4u16; let empty_sequencers = vec![]; let genesis_evm_block_hash = [0x11u8; 32]; - let operator_vk_hash = [0x22u8; 32]; + let program_history_root = [0x22u8; 32]; let commit0_op_return = ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - operator_vk_hash, + program_history_root, )); let commit0 = Transaction { version: Version::TWO, @@ -651,11 +542,11 @@ mod tests { let commit1_redeem_script = create_sequencer_update_script(¤t_pubkeys, current_threshold as usize); - let commit1_operator_vk_hash = [0x33u8; 32]; + let commit1_program_history_root = [0x33u8; 32]; let commit1_op_return = ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - commit1_operator_vk_hash, + commit1_program_history_root, )); let mut commit1 = Transaction { version: Version::TWO, @@ -707,11 +598,11 @@ mod tests { let commit2_redeem_script = create_sequencer_update_script(&next_pubkeys, next_threshold as usize); - let commit2_operator_vk_hash = [0x44u8; 32]; + let commit2_program_history_root = [0x44u8; 32]; let commit2_op_return = ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - commit2_operator_vk_hash, + commit2_program_history_root, )); let mut commit2 = Transaction { version: Version::TWO, @@ -773,21 +664,20 @@ 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); + assert_eq!(chain_state.block_height, 3); } #[test] - fn test_apply_commit_tracks_genesis_operator_vk_hash() { + fn test_apply_commit_enforces_new_genesis() { 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 program_history_root = [0x66u8; 32]; let commit_txn = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, @@ -804,7 +694,7 @@ mod tests { script_pubkey: ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - operator_vk_hash, + program_history_root, )), }, ], @@ -821,9 +711,15 @@ mod tests { block_height: 1, }; + let old_chain_commit = CircuitCommit { genesis_txid: [0x77; 32], ..commit.clone() }; + let result = std::panic::catch_unwind(|| { + CommitChainState::new(genesis_txid).apply_commit(vec![old_chain_commit]); + }); + assert!(result.is_err()); + let mut chain_state = CommitChainState::new(genesis_txid); chain_state.apply_commit(vec![commit]); - assert_eq!(chain_state.operator_vk_hash, operator_vk_hash); + assert_eq!(chain_state.block_height, 1); } } diff --git a/crates/commit-chain/src/lib.rs b/crates/commit-chain/src/lib.rs index 9b0b4f365..ccec01b9b 100644 --- a/crates/commit-chain/src/lib.rs +++ b/crates/commit-chain/src/lib.rs @@ -20,8 +20,7 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui ) .unwrap(); - let output: CommitChainCircuitOutput = - bincode::deserialize(&input.zkm_public_values).unwrap(); + let output = decode_commit_chain_circuit_output(&input.zkm_public_values); assert_eq!(output.self_program_id, previous_program_id); let history = verifier::next_history( verifier::ProgramType::Commit, @@ -31,23 +30,6 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui ); (output.chain_state, history) } - CommitChainPrevProofType::LegacyPrevProof => { - let previous_program_id = verifier::verify_groth16_proof( - &input.zkm_proof, - &input.zkm_public_values, - &input.zkm_vk_hash, - &input.zkm_version, - ) - .unwrap(); - let chain_state = - decode_pre_identity_commit_chain_output(&input.zkm_public_values).unwrap(); - let history = verifier::legacy_history( - verifier::ProgramType::Commit, - previous_program_id, - &input.zkm_public_values, - ); - (chain_state, history) - } }; chain_state.apply_commit(input.commits); @@ -57,25 +39,13 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui #[cfg(test)] mod circuit_output_tests { use super::*; - use serde::Serialize; - - #[derive(Serialize)] - struct LegacyOutput { - chain_state: CommitChainState, - } fn chain_state() -> CommitChainState { CommitChainState::new([1u8; 32]) } #[test] - fn classifies_only_current_and_immediate_legacy_outputs() { - let legacy = bincode::serialize(&LegacyOutput { chain_state: chain_state() }).unwrap(); - assert_eq!( - classify_commit_chain_output(&legacy).unwrap(), - CommitChainPrevProofType::LegacyPrevProof - ); - + fn classifies_only_current_outputs() { let current = bincode::serialize(&CommitChainCircuitOutput { chain_state: chain_state(), self_program_id: [1u8; 32], diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index 0960a1d42..4cc18f500 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -251,8 +251,6 @@ 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], #[arg(long, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] program_history_root: [u8; 32], }, @@ -269,8 +267,6 @@ 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, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] program_history_root: [u8; 32], #[arg(long)] @@ -382,7 +378,6 @@ async fn main() -> Result<(), Box> { publisher_btc_pubkeys, next_publisher_btc_pubkeys, goat_genesis_block_hash, - operator_vk_hash, program_history_root, } => { let (sequencer_set_hash, goat_block_number, cosmos_block_number) = @@ -404,7 +399,6 @@ async fn main() -> Result<(), Box> { update_connector, sequencer_set_hash, goat_genesis_block_hash, - operator_vk_hash, program_history_root, goat_block_number, ) @@ -417,7 +411,6 @@ async fn main() -> Result<(), Box> { next_publisher_btc_pubkeys, init_genesis, goat_genesis_block_hash, - operator_vk_hash, program_history_root, commit_info, } => { @@ -441,7 +434,6 @@ async fn main() -> Result<(), Box> { goat_block_number, sequencer_set_hash, goat_genesis_block_hash, - operator_vk_hash, program_history_root, output_file, ) @@ -553,11 +545,6 @@ fn next_commit_history_root( next_program_id, ) } - CommitChainPrevProofType::LegacyPrevProof => verifier::legacy_history( - verifier::ProgramType::Commit, - previous_program_id, - &public_values, - ), CommitChainPrevProofType::GenesisBlock => unreachable!(), }; Ok(verifier::finalize_history(verifier::ProgramType::Commit, history, next_program_id)) @@ -708,7 +695,6 @@ 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], program_history_root: [u8; 32], output_file: &str, ) -> Result<(), Box> { @@ -770,11 +756,10 @@ async fn action_push_sequencer_set_update( }; // Skip construction of the genesis tx - let mut commitment = [0u8; 128]; + let mut commitment = [0u8; 96]; commitment[0..32].copy_from_slice(&sequencer_set_hash); commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..96].copy_from_slice(&operator_vk_hash); - commitment[96..128].copy_from_slice(&program_history_root); + commitment[64..96].copy_from_slice(&program_history_root); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, &update_connector, @@ -820,7 +805,6 @@ 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], program_history_root: [u8; 32], goat_block_number: u64, ) -> Result<(), Box> { @@ -838,11 +822,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; 128]; + let mut commitment = [0u8; 96]; commitment[0..32].copy_from_slice(&sequencer_set_hash); commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..96].copy_from_slice(&operator_vk_hash); - commitment[96..128].copy_from_slice(&program_history_root); + commitment[64..96].copy_from_slice(&program_history_root); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, diff --git a/node/ssp-ci.sh b/node/ssp-ci.sh index 90f035e77..9f856c65c 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 --operator-vk-hash $OPERATOR_VK_HASH --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 --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 --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 +$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 # 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 --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 +$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 # 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 --operator-vk-hash $OPERATOR_VK_HASH --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 --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.2" From a26604244b934be5d3d4d2c0e6a88be8a79a8719 Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Thu, 23 Jul 2026 21:48:25 +0800 Subject: [PATCH 07/11] refactor(proof-builder): unify `program_id` implementation --- Cargo.lock | 1 + circuits/commit-chain-proof/host/src/lib.rs | 5 ----- circuits/header-chain-proof/host/src/lib.rs | 5 ----- circuits/operator-proof/host/src/lib.rs | 6 ------ circuits/proof-builder/Cargo.toml | 1 + circuits/proof-builder/src/lib.rs | 8 +++++++- circuits/state-chain-proof/host/src/lib.rs | 5 ----- circuits/watchtower-proof/host/src/lib.rs | 9 ++------- 8 files changed, 11 insertions(+), 29 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 537d2228d..976e36fa4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9753,6 +9753,7 @@ dependencies = [ "state-chain", "strum 0.26.3", "thiserror 1.0.69", + "verifier", "zkm-sdk", ] diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index cc89103fd..1f660ccbc 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -161,11 +161,6 @@ impl CommitChainProofBuilder { let (proving_key, verifying_key) = client.setup(COMMIT_CHAIN); Self { client, proving_key, verifying_key } } - - pub fn program_id(&self) -> anyhow::Result { - verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) - .map_err(anyhow::Error::msg) - } } impl ProofBuilder for CommitChainProofBuilder { diff --git a/circuits/header-chain-proof/host/src/lib.rs b/circuits/header-chain-proof/host/src/lib.rs index 213a7a67d..7801b90eb 100644 --- a/circuits/header-chain-proof/host/src/lib.rs +++ b/circuits/header-chain-proof/host/src/lib.rs @@ -168,11 +168,6 @@ impl HeaderChainProofBuilder { let (proving_key, verifying_key) = client.setup(HEADER_CHAIN); Self { client, proving_key, verifying_key } } - - pub fn program_id(&self) -> anyhow::Result { - verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) - .map_err(anyhow::Error::msg) - } } impl ProofBuilder for HeaderChainProofBuilder { diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 9588a969b..be3ddfffd 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -350,12 +350,6 @@ impl OperatorProofBuilder { let (proving_key, verifying_key) = client.setup(OPERATOR); Self { client, proving_key, verifying_key } } - - /// Returns the Program ID derived from this builder's verifying key. - pub fn program_id(&self) -> anyhow::Result { - verifier::program_id(self.verifying_key.bytes32().as_bytes(), zkm_sdk::ZKM_CIRCUIT_VERSION) - .map_err(anyhow::Error::msg) - } } impl ProofBuilder for OperatorProofBuilder { diff --git a/circuits/proof-builder/Cargo.toml b/circuits/proof-builder/Cargo.toml index cedd64512..f73139360 100644 --- a/circuits/proof-builder/Cargo.toml +++ b/circuits/proof-builder/Cargo.toml @@ -13,6 +13,7 @@ serde.workspace = true zkm-sdk.workspace = true #zkm-prover.workspace = true #zkm-verifier.workspace = true +verifier.workspace = true strum = { workspace = true, features = ["derive"] } # diff --git a/circuits/proof-builder/src/lib.rs b/circuits/proof-builder/src/lib.rs index d90bc5ae7..528fce114 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -7,7 +7,7 @@ use state_chain::CircuitStateBlock; use std::fs; use strum::{Display, EnumString}; use thiserror::Error; -use zkm_sdk::{ProverClient, ZKMProofWithPublicValues}; +use zkm_sdk::{HashableKey, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofWithPublicValues}; use zkm_sdk::{ZKMProvingKey, ZKMVerifyingKey}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -87,6 +87,12 @@ pub trait ProofBuilder { fn pk(&self) -> &ZKMProvingKey; fn vk(&self) -> &ZKMVerifyingKey; + /// Returns the Program ID derived from the builder's verifying key. + fn program_id(&self) -> Result { + verifier::program_id(self.vk().bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } + fn build_proof( &self, ctx: &ProofRequest, diff --git a/circuits/state-chain-proof/host/src/lib.rs b/circuits/state-chain-proof/host/src/lib.rs index d38f3ff22..40f4593c1 100644 --- a/circuits/state-chain-proof/host/src/lib.rs +++ b/circuits/state-chain-proof/host/src/lib.rs @@ -299,11 +299,6 @@ impl StateChainProofBuilder { let (proving_key, verifying_key) = client.setup(STATE_CHAIN); Self { client, proving_key, verifying_key } } - - pub fn program_id(&self) -> anyhow::Result { - verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) - .map_err(anyhow::Error::msg) - } } impl ProofBuilder for StateChainProofBuilder { diff --git a/circuits/watchtower-proof/host/src/lib.rs b/circuits/watchtower-proof/host/src/lib.rs index baf91f8de..a52da06da 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -5,8 +5,8 @@ use borsh::BorshDeserialize; use commit_chain::{CommitChainCircuitInput, CommitChainPrevProofType}; use header_chain::{CircuitBlockHeader, HeaderChainCircuitInput, HeaderChainPrevProofType}; use zkm_sdk::{ - HashableKey, Prover, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofKind, ZKMProofWithPublicValues, - ZKMStdin, include_elf, + HashableKey, Prover, ProverClient, ZKMProofKind, ZKMProofWithPublicValues, ZKMStdin, + include_elf, }; use bitcoin::{Block, Network, Transaction, Txid, hashes::Hash}; @@ -138,11 +138,6 @@ impl WatchtowerProofBuilder { let (proving_key, verifying_key) = client.setup(WATCHTOWER); Self { client, proving_key, verifying_key } } - - pub fn program_id(&self) -> anyhow::Result { - verifier::program_id(self.verifying_key.bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) - .map_err(anyhow::Error::msg) - } } impl ProofBuilder for WatchtowerProofBuilder { From 9021a358e199c26a57e4aac0ac69eb869283736e Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Fri, 24 Jul 2026 00:43:21 +0800 Subject: [PATCH 08/11] refactor(commit-chain): use hash to avoid the op_return length exceeding 80 bytes --- circuits/commit-chain-proof/host/src/lib.rs | 19 +- circuits/operator-proof/guest/src/main.rs | 4 +- .../bitcoin-light-client-circuit/src/lib.rs | 52 ++-- .../bitcoin-light-client-circuit/src/utils.rs | 2 +- crates/commit-chain/src/commit_chain.rs | 246 ++++++++++-------- node/src/bin/sequencer-set-publish.rs | 71 +++-- 6 files changed, 234 insertions(+), 160 deletions(-) diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 1f660ccbc..0706bbbeb 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -111,11 +111,18 @@ 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 commitment = parse_commit_chain_commitment(&op_return_data); - - if let tendermint::Hash::Sha256(expected_hash) = sequencer_hash(&ci.sequencers) { - assert_eq!(expected_hash, commitment.sequencer_set_hash); + let commitment = + extract_commit_chain_commitment(&commit_txn.output).map_err(anyhow::Error::msg)?; + if let tendermint::Hash::Sha256(sequencer_set_hash) = sequencer_hash(&ci.sequencers) { + anyhow::ensure!( + commitment + == commit_chain_commitment_digest( + sequencer_set_hash, + ci.genesis_evm_block_hash, + ci.program_history_root, + ), + "commit transaction digest does not match commit info" + ); } else { panic!("Invalid sequencer set hash"); } @@ -139,6 +146,8 @@ pub async fn fetch_commit_chain( next_publisher_public_keys, next_threshold: ci.next_threshold, genesis_txid: Txid::from_str(&ci.genesis_txid)?.as_raw_hash().to_byte_array(), + genesis_evm_block_hash: ci.genesis_evm_block_hash, + program_history_root: ci.program_history_root, block_height, }; commits.push(commit); diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index af42022f3..f95d59ba4 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -10,8 +10,8 @@ use std::str::FromStr; // Regenerate this ID after changing the Watchtower guest. const EXPECTED_WATCHTOWER_PROGRAM_ID: [u8; 32] = [ - 0x23, 0xf1, 0xb7, 0x2f, 0x13, 0x8b, 0xa8, 0x5c, 0xaf, 0x75, 0x83, 0xe6, 0x8c, 0xb4, 0x77, 0xec, - 0xe1, 0x2d, 0x22, 0x28, 0x5a, 0x2c, 0x51, 0x06, 0xdc, 0x63, 0xc4, 0x5c, 0x34, 0x53, 0x8c, 0x2e, + 0x38, 0x72, 0x34, 0xd6, 0x8c, 0x91, 0xae, 0x7f, 0xaa, 0x8e, 0xa7, 0x20, 0x70, 0x95, 0xdc, 0x36, + 0x4c, 0x58, 0xef, 0xd0, 0x47, 0x71, 0x5d, 0x62, 0x49, 0x86, 0x61, 0x88, 0xa8, 0x10, 0x10, 0x3b, ]; pub fn main() { diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index bf38dd58b..dfa5703a1 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -8,8 +8,8 @@ use alloy_primitives::U256; use bitcoin::Block; use bitcoin::hashes::{Hash, HashEngine, sha256}; use commit_chain::{ - CommitChainCircuitInput, decode_commit_chain_circuit_output, - extract_data_from_commitment_outputs, parse_commit_chain_commitment, sequencer_hash, + CommitChainCircuitInput, commit_chain_commitment_digest, decode_commit_chain_circuit_output, + extract_commit_chain_commitment, extract_data_from_commitment_outputs, sequencer_hash, }; use header_chain::{ BitcoinMerkleTree, BlockHeaderCircuitOutput, CircuitBlockHeader, CircuitTransaction, @@ -37,8 +37,8 @@ pub const EXPECTED_STATE_CHAIN_PROGRAM_ID: verifier::ProgramId = [ 0x82, 0x2c, 0x67, 0xf2, 0x97, 0x3a, 0x6f, 0xda, 0xef, 0x9f, 0x2b, 0xa6, 0xab, 0xb7, 0x3a, 0x17, ]; pub const EXPECTED_COMMIT_CHAIN_PROGRAM_ID: verifier::ProgramId = [ - 0x66, 0x40, 0xd5, 0x04, 0x68, 0x8f, 0x8f, 0xbb, 0x98, 0xbf, 0x42, 0x27, 0x9a, 0x50, 0x6c, 0x8a, - 0xe8, 0x0f, 0xd6, 0x67, 0xa0, 0x24, 0x6d, 0x43, 0xd1, 0xd7, 0x21, 0xce, 0xe6, 0xd9, 0x56, 0x41, + 0x99, 0xda, 0x42, 0x82, 0x30, 0x8f, 0x07, 0x42, 0x11, 0xd9, 0x59, 0x30, 0xd5, 0x26, 0x2c, 0xc2, + 0x4e, 0xd9, 0x13, 0x89, 0x79, 0xdf, 0x2e, 0x83, 0x59, 0xea, 0x6e, 0xc6, 0x8b, 0x91, 0xe3, 0x7d, ]; pub const GRAPH_ID_SIZE: usize = 16; pub const PROOF_SIZE: usize = 260; @@ -151,10 +151,9 @@ pub fn watch_longest_chain( let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); - // check commit chain's genesis block let commitment = - commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); - let commitment = parse_commit_chain_commitment(&commitment); + extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) + .expect("invalid commit-chain commitment output"); let program_history_root = verifier::program_history_root( checked_history_root( verifier::ProgramType::Header, @@ -178,17 +177,18 @@ pub fn watch_longest_chain( commit_chain_output.program_history_hash, ), ); - assert_eq!(commitment.program_history_root, program_history_root); - - if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment.sequencer_set_hash, x); + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + assert_eq!( + commitment, + commit_chain_commitment_digest( + sequencer_set_hash, + state_chain_output.chain_state.genesis_evm_block_hash, + program_history_root, + ) + ); } else { panic!("Invalid commitment: inconsistent sequencer set hash"); }; - assert_eq!( - commitment.genesis_evm_block_hash, - state_chain_output.chain_state.genesis_evm_block_hash - ); println!("commit public inputs"); // commit public inputs @@ -478,10 +478,9 @@ pub fn propose_longest_chain( let commit_sequencer_set_hash = sequencer_hash(&commit_chain_output.chain_state.sequencers); let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; - // check commit chain's genesis block let commitment = - commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); - let commitment = parse_commit_chain_commitment(&commitment); + extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) + .expect("invalid commit-chain commitment output"); let program_history_root = verifier::program_history_root( checked_history_root( verifier::ProgramType::Header, @@ -505,17 +504,18 @@ pub fn propose_longest_chain( commit_chain_output.program_history_hash, ), ); - assert_eq!(commitment.program_history_root, program_history_root); - - if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment.sequencer_set_hash, x); + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + assert_eq!( + commitment, + commit_chain_commitment_digest( + sequencer_set_hash, + state_chain_output.chain_state.genesis_evm_block_hash, + program_history_root, + ) + ); } else { panic!("Invalid commitment: inconsistent sequencer set 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); diff --git a/crates/bitcoin-light-client-circuit/src/utils.rs b/crates/bitcoin-light-client-circuit/src/utils.rs index e796e6ed2..01f45231f 100644 --- a/crates/bitcoin-light-client-circuit/src/utils.rs +++ b/crates/bitcoin-light-client-circuit/src/utils.rs @@ -82,7 +82,7 @@ pub fn create_fee_tx( } pub fn create_sequencer_update_partial_tx( - commitment: [u8; 96], + commitment: [u8; 32], update_connector: &Option, replenish_fee_connector: &Option, next_update_connector: Address, diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index e0fa0d975..f14c25d2a 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -11,6 +11,7 @@ pub use tendermint_light_client_verifier::{ use bincode::Options as BincodeOptions; use bitcoin::{Transaction, TxOut, Witness, hashes::Hash as _, secp256k1::PublicKey}; +use sha2::{Digest, Sha256}; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct CommitInfo { @@ -23,6 +24,8 @@ pub struct CommitInfo { pub txid: String, pub genesis_txid: String, pub sequencers: Vec, + pub genesis_evm_block_hash: [u8; 32], + pub program_history_root: [u8; 32], } /// The input proof of the commit chain circuit. @@ -44,6 +47,8 @@ pub struct CircuitCommit { #[serde(default)] pub next_threshold: Option, pub sequencers: Vec, + pub genesis_evm_block_hash: [u8; 32], + pub program_history_root: [u8; 32], pub block_height: u32, // Bitcoin block height of current commitment } @@ -115,14 +120,8 @@ impl CircuitCommit { pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; pub const VK_HASH_SIZE: usize = 66; -pub const COMMIT_CHAIN_COMMITMENT_SIZE: usize = 96; - -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] -pub struct CommitChainCommitment { - pub sequencer_set_hash: [u8; 32], - pub genesis_evm_block_hash: [u8; 32], - pub program_history_root: [u8; 32], -} +pub const COMMIT_CHAIN_COMMITMENT_SIZE: usize = 32; +const COMMIT_CHAIN_COMMITMENT_DOMAIN: &[u8] = b"bitvm2/commit-chain/v1"; #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct CommitChainCircuitOutput { @@ -166,25 +165,49 @@ pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> Hash { sequencer_set.hash() } -pub fn parse_commit_chain_commitment(commitment: &[u8]) -> CommitChainCommitment { - assert_eq!( - commitment.len(), - COMMIT_CHAIN_COMMITMENT_SIZE, - "commit chain commitment must be 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 program_history_root = [0u8; 32]; - program_history_root.copy_from_slice(&commitment[64..96]); - assert_ne!( - program_history_root, [0u8; 32], - "commit chain commitment must include non-zero program history root" - ); - - CommitChainCommitment { sequencer_set_hash, genesis_evm_block_hash, program_history_root } +pub fn commit_chain_commitment_digest( + sequencer_set_hash: [u8; 32], + genesis_evm_block_hash: [u8; 32], + program_history_root: [u8; 32], +) -> [u8; 32] { + assert_ne!(program_history_root, [0u8; 32], "program history root must be non-zero"); + let mut hasher = Sha256::new(); + hasher.update(COMMIT_CHAIN_COMMITMENT_DOMAIN); + hasher.update(sequencer_set_hash); + hasher.update(genesis_evm_block_hash); + hasher.update(program_history_root); + hasher.finalize().into() +} + +pub fn extract_commit_chain_commitment(tx_output: &[TxOut]) -> Result<[u8; 32], String> { + if tx_output.len() != 2 { + return Err(format!("commit transaction must have 2 outputs, got {}", tx_output.len())); + } + let output = &tx_output[1]; + if output.value != bitcoin::Amount::ZERO { + return Err("commitment output must have zero value".to_string()); + } + let instructions = output + .script_pubkey + .instructions_minimal() + .collect::, _>>() + .map_err(|err| format!("invalid commitment script: {err}"))?; + if instructions.len() != 2 { + return Err("commitment script must be OP_RETURN followed by one push".to_string()); + } + if !matches!( + instructions[0], + bitcoin::script::Instruction::Op(op) if op == bitcoin::opcodes::all::OP_RETURN + ) { + return Err("commitment output must start with OP_RETURN".to_string()); + } + let bitcoin::script::Instruction::PushBytes(bytes) = &instructions[1] else { + return Err("commitment must be pushed bytes".to_string()); + }; + bytes + .as_bytes() + .try_into() + .map_err(|_| format!("commitment must be {COMMIT_CHAIN_COMMITMENT_SIZE} bytes")) } /// Decode current commit-chain public values. @@ -226,11 +249,17 @@ impl CommitChainState { } // 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); + let actual_commitment = + extract_commit_chain_commitment(&latest_commit_txn_with_wtns.output).unwrap(); if let Hash::Sha256(latest_sequencer_set_hash) = sequencer_hash(latest_sequencers) { - assert_eq!(latest_sequencer_set_hash, latest_commitment.sequencer_set_hash); + assert_eq!( + actual_commitment, + commit_chain_commitment_digest( + latest_sequencer_set_hash, + commit.genesis_evm_block_hash, + commit.program_history_root, + ) + ); } else { panic!("Invalid latest sequencer set hash"); } @@ -238,15 +267,8 @@ impl CommitChainState { // check the latest txn's prev out is equals to the output of prev_txn let prev_commit_txn_value = &self.commit_txn; 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, prev_commitment.sequencer_set_hash); - } else { - panic!("Invalid prev sequencer set hash"); - } - + // The recursive proof or an earlier batch item already authenticated the previous + // commitment. let update_connector = &latest_commit_txn_with_wtns.input[0]; let prev_commit_txid = prev_commit_txn_value.compute_txid(); assert_eq!(update_connector.previous_output.txid, prev_commit_txid); @@ -311,28 +333,6 @@ pub fn extract_data_from_commitment_outputs(txouts: &[TxOut]) -> Vec { data } -pub fn extract_op_return_data(tx_output: &[TxOut]) -> Vec { - let mut results = Vec::new(); - for output in tx_output { - let script = &output.script_pubkey; - // Parse instructions from the script - let mut instructions = script.instructions(); - // First instruction should be OP_RETURN - if let Some(Ok(bitcoin::script::Instruction::Op(op))) = instructions.next() - && op == bitcoin::opcodes::all::OP_RETURN - { - // Next should be pushed data - if let Some(Ok(bitcoin::script::Instruction::PushBytes(data))) = instructions.next() { - results = data.as_bytes().to_vec(); - } - } - } - if results.is_empty() { - results = [0u8; 32].to_vec(); - } - results -} - #[cfg(test)] mod tests { use super::*; @@ -346,18 +346,20 @@ mod tests { }; #[test] 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 expected_op_data = [12; 32]; + let script = + ScriptBuf::new_op_return(PushBytesBuf::try_from(expected_op_data.to_vec()).unwrap()); let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, input: vec![], - output: vec![bitcoin::TxOut { value: Amount::ZERO, script_pubkey: script }], + output: vec![ + bitcoin::TxOut { value: Amount::from_sat(500), script_pubkey: ScriptBuf::new() }, + bitcoin::TxOut { value: Amount::ZERO, script_pubkey: script }, + ], }; - let op_return_data = extract_op_return_data(&tx.output); - assert_eq!(expected_op_data.to_vec(), op_return_data); + assert_eq!(expected_op_data, extract_commit_chain_commitment(&tx.output).unwrap()); } fn commitment_payload( @@ -365,52 +367,75 @@ mod tests { genesis_evm_block_hash: [u8; 32], program_history_root: [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(&program_history_root); - PushBytesBuf::try_from(payload).expect("commitment payload is pushable") + let sequencer_set_hash = + if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = + sequencer_hash(sequencers) + { + hash + } else { + panic!("expected sha256 sequencer hash"); + }; + PushBytesBuf::try_from( + commit_chain_commitment_digest( + sequencer_set_hash, + genesis_evm_block_hash, + program_history_root, + ) + .to_vec(), + ) + .expect("commitment payload is pushable") } #[test] - fn test_parse_commit_chain_commitment_splits_96_byte_payload() { - let sequencer_set_hash = [0x11u8; 32]; - let genesis_evm_block_hash = [0x22u8; 32]; - let program_history_root = [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(&program_history_root); - - 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.program_history_root, program_history_root); + fn test_commit_chain_commitment_digest_known_vector() { + assert_eq!( + hex::encode(commit_chain_commitment_digest([0x11; 32], [0x22; 32], [0x33; 32],)), + "28c3b9bb2c89dcc9867e43e457c5232f229b340823c34a84dc6ed0e8f88147e9" + ); } #[test] - fn test_parse_commit_chain_commitment_rejects_old_payload_sizes() { - for size in [64, 128] { - let payload = vec![0x11u8; size]; - let result = std::panic::catch_unwind(|| parse_commit_chain_commitment(&payload)); - assert!(result.is_err(), "{size}-byte payload must be rejected"); + fn test_extract_commit_chain_commitment_requires_canonical_push32() { + let digest = [0x11; 32]; + let valid = vec![ + TxOut { value: Amount::from_sat(500), script_pubkey: ScriptBuf::new() }, + TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::new_op_return( + PushBytesBuf::try_from(digest.to_vec()).unwrap(), + ), + }, + ]; + assert_eq!(extract_commit_chain_commitment(&valid).unwrap(), digest); + + for size in [31, 33, 64, 96, 128] { + let mut outputs = valid.clone(); + outputs[1].script_pubkey = + ScriptBuf::new_op_return(PushBytesBuf::try_from(vec![0x11; size]).unwrap()); + assert!(extract_commit_chain_commitment(&outputs).is_err(), "{size} bytes"); } + assert!(extract_commit_chain_commitment(&valid[..1]).is_err()); + let mut extra = valid.clone(); + extra.push(valid[1].clone()); + assert!(extract_commit_chain_commitment(&extra).is_err()); } #[test] - #[should_panic(expected = "commit chain commitment must include non-zero program history root")] - fn test_parse_commit_chain_commitment_rejects_zero_program_history_root() { - let mut payload = vec![0x11u8; 96]; - payload[64..].fill(0); + #[should_panic(expected = "program history root must be non-zero")] + fn test_commit_chain_commitment_digest_rejects_zero_program_history_root() { + commit_chain_commitment_digest([0x11; 32], [0x22; 32], [0; 32]); + } - parse_commit_chain_commitment(&payload); + #[test] + fn test_commit_info_requires_commitment_preimage_fields() { + let legacy = serde_json::json!({ + "threshold": 2, + "publisher_public_keys": [], + "txid": "00", + "genesis_txid": "00", + "sequencers": [] + }); + assert!(serde_json::from_value::(legacy).is_err()); } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -537,6 +562,8 @@ mod tests { next_publisher_public_keys: Some(current_pubkeys.clone()), next_threshold: Some(current_threshold), sequencers: empty_sequencers.clone(), + genesis_evm_block_hash, + program_history_root, block_height: 1, }; @@ -593,6 +620,8 @@ mod tests { next_publisher_public_keys: Some(next_pubkeys.clone()), next_threshold: Some(next_threshold), sequencers: empty_sequencers.clone(), + genesis_evm_block_hash, + program_history_root: commit1_program_history_root, block_height: 2, }; @@ -657,6 +686,8 @@ mod tests { next_publisher_public_keys: Some(final_pubkeys.clone()), next_threshold: Some(final_threshold), sequencers: empty_sequencers, + genesis_evm_block_hash, + program_history_root: commit2_program_history_root, block_height: 3, }; @@ -708,6 +739,8 @@ mod tests { next_publisher_public_keys: Some(next_pubkeys), next_threshold: Some(2), sequencers: empty_sequencers, + genesis_evm_block_hash, + program_history_root, block_height: 1, }; @@ -717,6 +750,13 @@ mod tests { }); assert!(result.is_err()); + let mismatched_preimage = + CircuitCommit { program_history_root: [0x77; 32], ..commit.clone() }; + let result = std::panic::catch_unwind(|| { + CommitChainState::new(genesis_txid).apply_commit(vec![mismatched_preimage]); + }); + assert!(result.is_err()); + let mut chain_state = CommitChainState::new(genesis_txid); chain_state.apply_commit(vec![commit]); diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index 4cc18f500..32f9fad4d 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -39,7 +39,9 @@ use bitcoin_light_client_circuit::{ use commit_chain::{ CommitChainCircuitOutput, CommitChainPrevProofType, classify_commit_chain_output, }; -use commit_chain::{CommitInfo, create_sequencer_update_script, finalize, sign_raw}; +use commit_chain::{ + CommitInfo, commit_chain_commitment_digest, create_sequencer_update_script, finalize, sign_raw, +}; use header_chain::{ BlockHeaderCircuitOutput, HeaderChainPrevProofType, classify_header_chain_output, }; @@ -122,6 +124,8 @@ async fn save_commit_info( sequencers: Vec, init_genesis: bool, commit_info_file: &str, + genesis_evm_block_hash: [u8; 32], + program_history_root: [u8; 32], ) -> Result<(), Box> { let file = std::fs::File::open(output_file)?; let output: OutputData = serde_json::from_reader(file).unwrap(); @@ -143,6 +147,8 @@ async fn save_commit_info( ), genesis_txid, sequencers: sequencers.iter().cloned().map(|v| v.into()).collect(), + genesis_evm_block_hash, + program_history_root, }; let commit_info = serde_json::to_string(&commit_info).unwrap(); @@ -221,8 +227,14 @@ enum Commands { header_chain_input_proof: String, #[arg(long)] state_chain_input_proof: String, - #[arg(long)] - commit_chain_input_proof: String, + #[arg( + long, + required_unless_present = "commit_chain_genesis", + conflicts_with = "commit_chain_genesis" + )] + commit_chain_input_proof: Option, + #[arg(long, default_value_t = false, conflicts_with = "commit_chain_input_proof")] + commit_chain_genesis: bool, #[arg(long, value_parser = hex_parse::<32>)] next_header_program_id: [u8; 32], #[arg(long, value_parser = hex_parse::<32>)] @@ -299,6 +311,7 @@ async fn main() -> Result<(), Box> { header_chain_input_proof, state_chain_input_proof, commit_chain_input_proof, + commit_chain_genesis, next_header_program_id, next_state_program_id, next_commit_program_id, @@ -307,7 +320,8 @@ async fn main() -> Result<(), Box> { let root = derive_program_history_root( header_chain_input_proof, state_chain_input_proof, - commit_chain_input_proof, + commit_chain_input_proof.as_deref(), + *commit_chain_genesis, *next_header_program_id, *next_state_program_id, *next_commit_program_id, @@ -438,22 +452,18 @@ async fn main() -> Result<(), Box> { output_file, ) .await?; - match save_commit_info( + save_commit_info( &args.output_file, &publisher_btc_pubkeys, &next_publisher_btc_pubkeys, sequencers, init_genesis, &commit_info, + goat_genesis_block_hash, + program_history_root, ) - .await - { - Err(e) => { - println!("Failed to save commit info: {e}, commit_info: {commit_info}"); - Ok(()) - } - _ => Ok(()), - } + .await?; + Ok(()) } } } @@ -554,15 +564,28 @@ fn next_commit_history_root( fn derive_program_history_root( header_path: &str, state_path: &str, - commit_path: &str, + commit_path: Option<&str>, + commit_chain_genesis: bool, next_header_program_id: verifier::ProgramId, next_state_program_id: verifier::ProgramId, next_commit_program_id: verifier::ProgramId, ) -> anyhow::Result<[u8; 32]> { + let commit_history_root = if commit_chain_genesis { + verifier::finalize_history( + verifier::ProgramType::Commit, + verifier::initial_history(verifier::ProgramType::Commit), + next_commit_program_id, + ) + } else { + next_commit_history_root( + commit_path.expect("commit proof is required unless genesis is selected"), + next_commit_program_id, + )? + }; Ok(verifier::program_history_root( next_header_history_root(header_path, next_header_program_id)?, next_state_history_root(state_path, next_state_program_id)?, - next_commit_history_root(commit_path, next_commit_program_id)?, + commit_history_root, )) } @@ -756,10 +779,11 @@ async fn action_push_sequencer_set_update( }; // Skip construction of the genesis tx - let mut commitment = [0u8; 96]; - commitment[0..32].copy_from_slice(&sequencer_set_hash); - commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..96].copy_from_slice(&program_history_root); + let commitment = commit_chain_commitment_digest( + sequencer_set_hash, + goat_genesis_block_hash, + program_history_root, + ); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, &update_connector, @@ -822,10 +846,11 @@ 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; 96]; - commitment[0..32].copy_from_slice(&sequencer_set_hash); - commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..96].copy_from_slice(&program_history_root); + let commitment = commit_chain_commitment_digest( + sequencer_set_hash, + goat_genesis_block_hash, + program_history_root, + ); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, From b7e8a0dccba64bb180ac0d405ee82dc27330531e Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Fri, 24 Jul 2026 23:23:42 +0800 Subject: [PATCH 09/11] fix clippy --- node/src/bin/sequencer-set-publish.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index 32f9fad4d..bbccdd78c 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -117,6 +117,7 @@ impl OutputData { } } +#[allow(clippy::too_many_arguments)] async fn save_commit_info( output_file: &str, btc_public_keys: &[secp256k1::PublicKey], From 4ee086faa474c1198bfbe512a909849314ee013b Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Sun, 26 Jul 2026 21:28:59 +0800 Subject: [PATCH 10/11] refactor: remove legacy proof handling and improve public value deserialization --- crates/header-chain/src/header_chain.rs | 29 ++++++++++++------------ crates/header-chain/src/lib.rs | 30 +++++-------------------- crates/state-chain/src/lib.rs | 30 +++++-------------------- crates/state-chain/src/state_chain.rs | 29 ++++++++++++------------ crates/verifier/src/lib.rs | 12 ---------- node/src/bin/sequencer-set-publish.rs | 20 +++++------------ 6 files changed, 48 insertions(+), 102 deletions(-) diff --git a/crates/header-chain/src/header_chain.rs b/crates/header-chain/src/header_chain.rs index e3b081162..0583e1eb0 100644 --- a/crates/header-chain/src/header_chain.rs +++ b/crates/header-chain/src/header_chain.rs @@ -1,4 +1,5 @@ use crate::MMRGuest; +use bincode::Options as BincodeOptions; use bitcoin::{ BlockHash, CompactTarget, TxMerkleNode, block::{Header, Version}, @@ -367,36 +368,36 @@ pub struct BlockHeaderCircuitOutput { pub program_history_hash: [u8; 32], } -#[derive(Deserialize)] -struct LegacyBlockHeaderCircuitOutput { - chain_state: ChainState, -} - /// The input proof of the header chain circuit. /// The proof can be either None (implying the beginning) or a Succinct proof. #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, BorshDeserialize, BorshSerialize)] pub enum HeaderChainPrevProofType { GenesisBlock, PrevProof, - LegacyPrevProof, } pub fn classify_header_chain_output( public_values: &[u8], ) -> Result { - if bincode::deserialize::(public_values).is_ok() { + if deserialize_header_chain_output(public_values).is_ok() { return Ok(HeaderChainPrevProofType::PrevProof); } - if bincode::deserialize::(public_values).is_ok() { - return Ok(HeaderChainPrevProofType::LegacyPrevProof); - } Err("unknown header-chain public output format".to_string()) } -pub fn decode_legacy_header_chain_output(public_values: &[u8]) -> Result { - bincode::deserialize::(public_values) - .map(|output| output.chain_state) - .map_err(|err| format!("invalid legacy header-chain output: {err}")) +fn deserialize_header_chain_output( + public_values: &[u8], +) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .reject_trailing_bytes() + .deserialize(public_values) +} + +/// Decode current header-chain public values. +pub fn decode_header_chain_circuit_output(public_values: &[u8]) -> BlockHeaderCircuitOutput { + deserialize_header_chain_output(public_values) + .expect("failed to decode current header chain circuit output") } /// The input of the header chain circuit. diff --git a/crates/header-chain/src/lib.rs b/crates/header-chain/src/lib.rs index bf6234204..25bb156e4 100644 --- a/crates/header-chain/src/lib.rs +++ b/crates/header-chain/src/lib.rs @@ -32,8 +32,7 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui ) .unwrap(); - let output: BlockHeaderCircuitOutput = - bincode::deserialize(&input.zkm_public_values).unwrap(); + let output = decode_header_chain_circuit_output(&input.zkm_public_values); assert_eq!(output.self_program_id, previous_program_id); let history = verifier::next_history( verifier::ProgramType::Header, @@ -43,22 +42,6 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui ); (output.chain_state, history) } - HeaderChainPrevProofType::LegacyPrevProof => { - let previous_program_id = verifier::verify_groth16_proof( - &input.zkm_proof, - &input.zkm_public_values, - &input.zkm_vk_hash, - &input.zkm_version, - ) - .unwrap(); - let chain_state = decode_legacy_header_chain_output(&input.zkm_public_values).unwrap(); - let history = verifier::legacy_history( - verifier::ProgramType::Header, - previous_program_id, - &input.zkm_public_values, - ); - (chain_state, history) - } }; chain_state.apply_blocks(input.block_headers); @@ -76,14 +59,11 @@ mod circuit_output_tests { } #[test] - fn classifies_only_current_and_immediate_legacy_outputs() { + fn classifies_only_strict_current_outputs() { let legacy = bincode::serialize(&LegacyOutput { chain_state: ChainState::new() }).unwrap(); - assert_eq!( - classify_header_chain_output(&legacy).unwrap(), - HeaderChainPrevProofType::LegacyPrevProof - ); + assert!(classify_header_chain_output(&legacy).is_err()); - let current = bincode::serialize(&BlockHeaderCircuitOutput { + let mut current = bincode::serialize(&BlockHeaderCircuitOutput { chain_state: ChainState::new(), self_program_id: [1u8; 32], program_history_hash: [2u8; 32], @@ -93,6 +73,8 @@ mod circuit_output_tests { classify_header_chain_output(¤t).unwrap(), HeaderChainPrevProofType::PrevProof ); + current.push(0); + assert!(classify_header_chain_output(¤t).is_err()); assert!(classify_header_chain_output(b"unknown").is_err()); } } diff --git a/crates/state-chain/src/lib.rs b/crates/state-chain/src/lib.rs index aa14ac054..3e802f18d 100644 --- a/crates/state-chain/src/lib.rs +++ b/crates/state-chain/src/lib.rs @@ -26,8 +26,7 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu ) .unwrap(); - let state_chain_output: StateChainCircuitOutput = - bincode::deserialize(&input.zkm_public_values).unwrap(); + let state_chain_output = decode_state_chain_circuit_output(&input.zkm_public_values); assert_eq!(state_chain_output.self_program_id, previous_program_id); let history = verifier::next_history( verifier::ProgramType::State, @@ -37,22 +36,6 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu ); (state_chain_output.chain_state, history) } - StateChainPrevProofType::LegacyPrevProof => { - let previous_program_id = verifier::verify_groth16_proof( - &input.zkm_proof, - &input.zkm_public_values, - &input.zkm_vk_hash, - &input.zkm_version, - ) - .unwrap(); - let chain_state = decode_legacy_state_chain_output(&input.zkm_public_values).unwrap(); - let history = verifier::legacy_history( - verifier::ProgramType::State, - previous_program_id, - &input.zkm_public_values, - ); - (chain_state, history) - } }; chain_state.apply_blocks(input.blocks); @@ -74,14 +57,11 @@ mod circuit_output_tests { } #[test] - fn classifies_only_current_and_immediate_legacy_outputs() { + fn classifies_only_strict_current_outputs() { let legacy = bincode::serialize(&LegacyOutput { chain_state: chain_state() }).unwrap(); - assert_eq!( - classify_state_chain_output(&legacy).unwrap(), - StateChainPrevProofType::LegacyPrevProof - ); + assert!(classify_state_chain_output(&legacy).is_err()); - let current = bincode::serialize(&StateChainCircuitOutput { + let mut current = bincode::serialize(&StateChainCircuitOutput { chain_state: chain_state(), self_program_id: [1u8; 32], program_history_hash: [2u8; 32], @@ -91,6 +71,8 @@ mod circuit_output_tests { classify_state_chain_output(¤t).unwrap(), StateChainPrevProofType::PrevProof ); + current.push(0); + assert!(classify_state_chain_output(¤t).is_err()); assert!(classify_state_chain_output(b"unknown").is_err()); } } diff --git a/crates/state-chain/src/state_chain.rs b/crates/state-chain/src/state_chain.rs index c99a19212..8e118eb68 100644 --- a/crates/state-chain/src/state_chain.rs +++ b/crates/state-chain/src/state_chain.rs @@ -2,6 +2,7 @@ use crate::cbft::check_el_block_from_payload; use alloy_consensus::Header; use alloy_primitives::utils::keccak256; use alloy_primitives::{Address, B256, U256}; +use bincode::Options as BincodeOptions; use guest_executor::executor::EthClientExecutor; use guest_executor::io::EthClientExecutorInput; use serde::{Deserialize, Serialize}; @@ -17,7 +18,6 @@ type WithdrawalSlot = (Address, [u8; 32], Vec<[u8; 16]>); pub enum StateChainPrevProofType { GenesisBlock, PrevProof, - LegacyPrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -47,27 +47,28 @@ pub struct StateChainCircuitOutput { pub program_history_hash: [u8; 32], } -#[derive(Deserialize)] -struct LegacyStateChainCircuitOutput { - chain_state: StateChainState, -} - pub fn classify_state_chain_output( public_values: &[u8], ) -> Result { - if bincode::deserialize::(public_values).is_ok() { + if deserialize_state_chain_output(public_values).is_ok() { return Ok(StateChainPrevProofType::PrevProof); } - if bincode::deserialize::(public_values).is_ok() { - return Ok(StateChainPrevProofType::LegacyPrevProof); - } Err("unknown state-chain public output format".to_string()) } -pub fn decode_legacy_state_chain_output(public_values: &[u8]) -> Result { - bincode::deserialize::(public_values) - .map(|output| output.chain_state) - .map_err(|err| format!("invalid legacy state-chain output: {err}")) +fn deserialize_state_chain_output( + public_values: &[u8], +) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .reject_trailing_bytes() + .deserialize(public_values) +} + +/// Decode current state-chain public values. +pub fn decode_state_chain_circuit_output(public_values: &[u8]) -> StateChainCircuitOutput { + deserialize_state_chain_output(public_values) + .expect("failed to decode current state chain circuit output") } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] diff --git a/crates/verifier/src/lib.rs b/crates/verifier/src/lib.rs index b024052fa..6219b708f 100644 --- a/crates/verifier/src/lib.rs +++ b/crates/verifier/src/lib.rs @@ -34,18 +34,6 @@ pub fn initial_history(program_type: ProgramType) -> [u8; 32] { tagged_hash(b"bitvm2/vk-history-seed/v1", &[&[program_type as u8]]) } -pub fn legacy_history( - program_type: ProgramType, - previous_program_id: ProgramId, - previous_public_values: &[u8], -) -> [u8; 32] { - let public_values_hash: [u8; 32] = Sha256::digest(previous_public_values).into(); - tagged_hash( - b"bitvm2/vk-history-migration/v1", - &[&[program_type as u8], &previous_program_id, &public_values_hash], - ) -} - pub fn next_history( program_type: ProgramType, previous_history: [u8; 32], diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index bbccdd78c..3b9a5c824 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -43,9 +43,11 @@ use commit_chain::{ CommitInfo, commit_chain_commitment_digest, create_sequencer_update_script, finalize, sign_raw, }; use header_chain::{ - BlockHeaderCircuitOutput, HeaderChainPrevProofType, classify_header_chain_output, + HeaderChainPrevProofType, classify_header_chain_output, decode_header_chain_circuit_output, +}; +use state_chain::{ + StateChainPrevProofType, classify_state_chain_output, decode_state_chain_circuit_output, }; -use state_chain::{StateChainCircuitOutput, StateChainPrevProofType, classify_state_chain_output}; use tendermint::validator::Info; use reqwest::Url; @@ -486,7 +488,7 @@ fn next_header_history_root( let (public_values, previous_program_id) = load_verified_proof(path)?; let history = match classify_header_chain_output(&public_values).map_err(anyhow::Error::msg)? { HeaderChainPrevProofType::PrevProof => { - let output: BlockHeaderCircuitOutput = bincode::deserialize(&public_values)?; + let output = decode_header_chain_circuit_output(&public_values); anyhow::ensure!( output.self_program_id == previous_program_id, "header ProgramId mismatch" @@ -498,11 +500,6 @@ fn next_header_history_root( next_program_id, ) } - HeaderChainPrevProofType::LegacyPrevProof => verifier::legacy_history( - verifier::ProgramType::Header, - previous_program_id, - &public_values, - ), HeaderChainPrevProofType::GenesisBlock => unreachable!(), }; Ok(verifier::finalize_history(verifier::ProgramType::Header, history, next_program_id)) @@ -515,7 +512,7 @@ fn next_state_history_root( let (public_values, previous_program_id) = load_verified_proof(path)?; let history = match classify_state_chain_output(&public_values).map_err(anyhow::Error::msg)? { StateChainPrevProofType::PrevProof => { - let output: StateChainCircuitOutput = bincode::deserialize(&public_values)?; + let output = decode_state_chain_circuit_output(&public_values); anyhow::ensure!( output.self_program_id == previous_program_id, "state ProgramId mismatch" @@ -527,11 +524,6 @@ fn next_state_history_root( next_program_id, ) } - StateChainPrevProofType::LegacyPrevProof => verifier::legacy_history( - verifier::ProgramType::State, - previous_program_id, - &public_values, - ), StateChainPrevProofType::GenesisBlock => unreachable!(), }; Ok(verifier::finalize_history(verifier::ProgramType::State, history, next_program_id)) From 5bdfa871d7e43330118b64b3289a75ff365029a1 Mon Sep 17 00:00:00 2001 From: Blake <0xblake.sg@gmail.com> Date: Thu, 30 Jul 2026 15:46:11 +0800 Subject: [PATCH 11/11] feat: add upgrade checkpoint hash to state/header chain and implement upgrade commit loading --- Cargo.lock | 2 - circuits/commit-chain-proof/host/src/lib.rs | 127 ++++- circuits/commit-chain-proof/host/src/main.rs | 9 +- circuits/operator-proof/guest/src/main.rs | 33 +- circuits/operator-proof/host/src/lib.rs | 131 ++--- circuits/operator-proof/host/src/main.rs | 16 +- circuits/proof-builder/src/lib.rs | 10 +- .../bitcoin-light-client-circuit/src/lib.rs | 533 +++++++++--------- .../src/signature.rs | 94 ++- crates/commit-chain/src/commit_chain.rs | 171 ++++-- crates/commit-chain/src/lib.rs | 23 +- crates/header-chain/src/header_chain.rs | 16 +- crates/header-chain/src/lib.rs | 48 +- crates/header-chain/src/mmr.rs | 99 ++-- crates/header-chain/src/spv.rs | 13 +- crates/state-chain/src/cbft.rs | 17 +- crates/state-chain/src/lib.rs | 37 +- crates/state-chain/src/state_chain.rs | 1 + crates/store/src/localdb.rs | 12 + crates/verifier/src/lib.rs | 78 ++- node/Cargo.toml | 2 - node/src/bin/sequencer-set-publish.rs | 310 ++++++---- node/src/handle.rs | 15 +- node/src/utils.rs | 72 ++- node/src/vk.rs | 118 +--- .../src/task/commit_chain_proof.rs | 65 ++- proof-builder-rpc/src/task/mod.rs | 145 ++++- proof-builder-rpc/src/task/operator_proof.rs | 16 +- 28 files changed, 1350 insertions(+), 863 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 976e36fa4..7392b8b47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3095,7 +3095,6 @@ dependencies = [ "clap", "client", "commit-chain", - "dirs", "dotenv", "esplora-client", "futures", @@ -3105,7 +3104,6 @@ dependencies = [ "http 1.4.0", "http-body-util", "indexmap 2.14.0", - "indicatif", "libp2p", "libp2p-metrics", "libp2p-swarm-derive", diff --git a/circuits/commit-chain-proof/host/src/lib.rs b/circuits/commit-chain-proof/host/src/lib.rs index 0706bbbeb..e1f640633 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -49,6 +49,9 @@ pub struct Args { #[arg(long, default_value = "commits.bin")] pub commits: String, + #[arg(long, env)] + pub upgrade_commits: Option, + #[clap(long, env, default_value_t = 1)] pub batch_size: usize, @@ -65,6 +68,13 @@ pub struct Args { pub output_proof: String, } +impl Args { + /// Returns whether this request must rebuild from Commit Genesis. + pub fn starts_from_genesis(&self) -> bool { + self.init_input || self.upgrade_commits.is_some() + } +} + impl LongRunning for Args { fn rotate(&self) -> Self { let mut next_args = self.clone(); @@ -83,6 +93,7 @@ impl LongRunning for Args { next_args.start, ); next_args.commits = format!("{}.commits", next_args.input_proof); + next_args.upgrade_commits = None; next_args } } @@ -120,6 +131,8 @@ pub async fn fetch_commit_chain( sequencer_set_hash, ci.genesis_evm_block_hash, ci.program_history_root, + ci.proof_checkpoint_root, + ci.authorized_program_ids, ), "commit transaction digest does not match commit info" ); @@ -149,6 +162,8 @@ pub async fn fetch_commit_chain( genesis_evm_block_hash: ci.genesis_evm_block_hash, program_history_root: ci.program_history_root, block_height, + proof_checkpoint_root: ci.proof_checkpoint_root, + authorized_program_ids: ci.authorized_program_ids, }; commits.push(commit); std::fs::write(commits_file, serde_json::to_vec(&commits)?) @@ -156,6 +171,28 @@ pub async fn fetch_commit_chain( Ok(commits) } +/// Loads a full Genesis-to-latest commit replay for a Commit ProgramId upgrade. +pub fn load_upgrade_commits( + path: &str, + latest_commit: &CircuitCommit, +) -> anyhow::Result> { + let commits: Vec = + serde_json::from_slice(&std::fs::read(path).with_context(|| format!("read {path}"))?) + .with_context(|| format!("parse {path}"))?; + anyhow::ensure!(!commits.is_empty(), "upgrade commit replay must be non-empty"); + anyhow::ensure!( + commits[0].commit_txn.compute_txid().as_byte_array() == &commits[0].genesis_txid, + "upgrade replay must start at its fixed Genesis" + ); + anyhow::ensure!( + commits.last().map(|commit| commit.commit_txn.compute_txid()) + == Some(latest_commit.commit_txn.compute_txid()), + "upgrade replay final transaction does not match current commit info" + ); + + Ok(commits) +} + /// A program that aggregates the proofs of the simple program. pub struct CommitChainProofBuilder { client: ProverClient, @@ -200,7 +237,7 @@ impl ProofBuilder for CommitChainProofBuilder { }; //let mut zkm_vk_hash = self.verifying_key.hash_u32(); - // Set the previous proof type based on input_proof argument + // Genesis replay deliberately skips every predecessor proof sidecar. let prev_receipt = if *init_input { None } else { @@ -292,7 +329,7 @@ impl ProofBuilder for CommitChainProofBuilder { fn save_proof( &self, ctx: &proof_builder::ProofRequest, - _input: &[u8], + input: &[u8], _cycles: u64, proof: ZKMProofWithPublicValues, ) -> anyhow::Result<(String, usize)> { @@ -317,6 +354,11 @@ impl ProofBuilder for CommitChainProofBuilder { )?; std::fs::write(format!("{}.vk_hash.bin", output_proof), self.verifying_key.bytes32())?; std::fs::write(format!("{}.zkm_version.bin", output_proof), zkm_version)?; + let circuit_input: CommitChainCircuitInput = bincode::deserialize(input)?; + std::fs::write( + format!("{}.commits", output_proof), + serde_json::to_vec(&circuit_input.commits)?, + )?; Ok((public_value_hex, proof_size)) } } @@ -325,8 +367,89 @@ impl ProofBuilder for CommitChainProofBuilder { mod tests { use super::*; + use bitcoin::{Transaction, absolute::LockTime, transaction::Version}; use tracing::info; + fn circuit_commit(transaction: Transaction, genesis_txid: [u8; 32]) -> CircuitCommit { + CircuitCommit { + commit_txn: transaction, + genesis_txid, + publisher_public_keys: vec![], + threshold: 0, + next_publisher_public_keys: None, + next_threshold: None, + sequencers: vec![], + genesis_evm_block_hash: [0; 32], + program_history_root: [1; 32], + block_height: 0, + proof_checkpoint_root: [1; 32], + authorized_program_ids: AuthorizedProgramIds { + header: [1; 32], + state: [2; 32], + commit: [3; 32], + watchtower: [4; 32], + }, + } + } + + #[test] + fn upgrade_replay_starts_from_genesis_once() { + let args = Args::try_parse_from([ + "commit-chain-proof", + "--commit-info", + "commit-info.json", + "--upgrade-commits", + "upgrade-commits.json", + ]) + .unwrap(); + assert!(args.starts_from_genesis()); + + let next = args.rotate(); + assert!(!next.starts_from_genesis()); + } + + #[test] + fn upgrade_replay_requires_fixed_genesis_and_latest_commit() { + let transaction = Transaction { + version: Version::TWO, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + }; + let genesis_txid = transaction.compute_txid().to_byte_array(); + let commit = circuit_commit(transaction, genesis_txid); + let path = std::env::temp_dir().join(format!( + "commit-upgrade-replay-{}-{}.json", + std::process::id(), + std::thread::current().name().unwrap_or("test") + )); + + std::fs::write(&path, serde_json::to_vec(&vec![commit.clone()]).unwrap()).unwrap(); + assert_eq!( + load_upgrade_commits(path.to_str().unwrap(), &commit).unwrap(), + vec![commit.clone()] + ); + + let other_latest = circuit_commit( + Transaction { + version: Version::ONE, + lock_time: LockTime::ZERO, + input: vec![], + output: vec![], + }, + genesis_txid, + ); + assert!(load_upgrade_commits(path.to_str().unwrap(), &other_latest).is_err()); + + let wrong_genesis = CircuitCommit { genesis_txid: [9; 32], ..commit.clone() }; + std::fs::write(&path, serde_json::to_vec(&vec![wrong_genesis]).unwrap()).unwrap(); + assert!(load_upgrade_commits(path.to_str().unwrap(), &commit).is_err()); + + std::fs::write(&path, serde_json::to_vec(&Vec::::new()).unwrap()).unwrap(); + assert!(load_upgrade_commits(path.to_str().unwrap(), &commit).is_err()); + std::fs::remove_file(path).unwrap(); + } + #[test] #[ignore = "local test"] fn test_parse_commit_chain_proof() { diff --git a/circuits/commit-chain-proof/host/src/main.rs b/circuits/commit-chain-proof/host/src/main.rs index ab72bbe96..4e4182044 100644 --- a/circuits/commit-chain-proof/host/src/main.rs +++ b/circuits/commit-chain-proof/host/src/main.rs @@ -1,6 +1,6 @@ //! Generate commit chain proof use clap::Parser; -use commit_chain_proof::{Args, CommitChainProofBuilder, fetch_commit_chain}; +use commit_chain_proof::{Args, CommitChainProofBuilder, fetch_commit_chain, load_upgrade_commits}; use proof_builder::{ProofBuilder, ProofRequest}; #[tokio::main] @@ -17,7 +17,7 @@ async fn main() { return; } - let commits = fetch_commit_chain( + let mut commits = fetch_commit_chain( &args.esplora_url, &args.commit_info, &args.commits, @@ -25,8 +25,11 @@ async fn main() { ) .await .unwrap(); + if let Some(path) = args.upgrade_commits.as_deref() { + commits = load_upgrade_commits(path, &commits[0]).unwrap(); + } let ctx = ProofRequest::CommitChainProofRequest { - init_input: args.init_input, + init_input: args.starts_from_genesis(), input_proof: args.input_proof.clone(), output_proof: args.output_proof.clone(), commit_info: args.commit_info.clone(), diff --git a/circuits/operator-proof/guest/src/main.rs b/circuits/operator-proof/guest/src/main.rs index f95d59ba4..51e7cfd52 100644 --- a/circuits/operator-proof/guest/src/main.rs +++ b/circuits/operator-proof/guest/src/main.rs @@ -1,35 +1,23 @@ #![no_main] zkm_zkvm::entrypoint!(main); -use alloy_primitives::{Address, U256}; -use bitcoin::{ScriptBuf, Transaction, TxOut}; -use bitcoin_light_client_circuit::EthClientExecutorInput; +use alloy_primitives::U256; +use bitcoin::Transaction; +use bitcoin_light_client_circuit::IndexedWatchtowerChallenge; use commit_chain::CommitChainCircuitInput; use header_chain::{HeaderChainCircuitInput, SPV}; use state_chain::StateChainCircuitInput; -use std::str::FromStr; - -// Regenerate this ID after changing the Watchtower guest. -const EXPECTED_WATCHTOWER_PROGRAM_ID: [u8; 32] = [ - 0x38, 0x72, 0x34, 0xd6, 0x8c, 0x91, 0xae, 0x7f, 0xaa, 0x8e, 0xa7, 0x20, 0x70, 0x95, 0xdc, 0x36, - 0x4c, 0x58, 0xef, 0xd0, 0x47, 0x71, 0x5d, 0x62, 0x49, 0x86, 0x61, 0x88, 0xa8, 0x10, 0x10, 0x3b, -]; 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::(); let graph_id: [u8; 16] = zkm_zkvm::io::read::<[u8; 16]>(); let operator_genesis_sequencer_commit_txid: [u8; 32] = zkm_zkvm::io::read(); - println!("read operator commit txn"); - let operator_latest_sequencer_commit_txn: Transaction = zkm_zkvm::io::read(); // private inputs - let latest_sequencer_commit_txid = operator_latest_sequencer_commit_txn.compute_txid(); // public input // https://github.com/KSlashh/BitVM/blob/v2/goat/src/transactions/watchtower_challenge.rs#L128 - let watchtower_challenge_indices: Vec = zkm_zkvm::io::read(); + let watchtower_challenge_init_txid: [u8; 32] = zkm_zkvm::io::read(); + let watchtower_challenge_init_txn: Option = zkm_zkvm::io::read(); let graph_watchtower_xonly_public_keys: Vec<[u8; 32]> = zkm_zkvm::io::read(); - let watchtower_challenge_txns: Vec = zkm_zkvm::io::read(); - let watchtower_challenge_txn_pubkey: Vec = zkm_zkvm::io::read(); - let watchtower_challenge_txn_scripts: Vec = zkm_zkvm::io::read(); - let watchtower_challenge_txn_prev_outs: Vec = zkm_zkvm::io::read(); + let watchtower_challenges: Vec = zkm_zkvm::io::read(); let operator_header_chain: HeaderChainCircuitInput = zkm_zkvm::io::read(); let operator_commit_chain: CommitChainCircuitInput = zkm_zkvm::io::read(); @@ -42,13 +30,10 @@ 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, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenges, &graph_watchtower_xonly_public_keys, - EXPECTED_WATCHTOWER_PROGRAM_ID, operator_header_chain, operator_commit_chain, operator_state_chain, diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index be3ddfffd..eca6e2195 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -1,13 +1,8 @@ //! Generate operator proof use alloy_primitives::U256; use anyhow::Context; -use bitcoin::{ - BlockHash, Network, ScriptBuf, Transaction, TxOut, Txid, - hashes::Hash, - secp256k1::{PublicKey, XOnlyPublicKey}, -}; -use bitcoin_light_client_circuit::build_spv; -use bitcoin_script::script; +use bitcoin::{Block, BlockHash, Network, Transaction, Txid, hashes::Hash, secp256k1::PublicKey}; +use bitcoin_light_client_circuit::{IndexedWatchtowerChallenge, build_spv}; use borsh::BorshDeserialize; use clap::Parser; use client::btc_chain::BTCClient; @@ -172,7 +167,7 @@ use sha2::{Digest, Sha256}; use std::sync::OnceLock; static ELF_ID: OnceLock = OnceLock::new(); -type IndexedWatchtowerInputs = Vec<(u16, Txid, PublicKey)>; +type IndexedWatchtowerInputs = Vec<(u16, Txid)>; type GraphWatchtowerXOnlyPublicKeys = Vec<[u8; 32]>; /// Parses the full graph key list and keeps each included challenge's original graph index. @@ -201,7 +196,7 @@ fn parse_indexed_watchtower_inputs( .iter() .enumerate() .filter(|(_, txid)| !txid.trim().is_empty()) - .map(|(index, txid)| Ok((index as u16, Txid::from_str(txid)?, public_keys[index]))) + .map(|(index, txid)| Ok((index as u16, Txid::from_str(txid)?))) .collect::>>()?; Ok((included, graph_keys)) @@ -220,15 +215,14 @@ pub async fn fetch_target_block_and_watchtower_tx( bitcoin::Block, BlockHash, Transaction, - Vec, Vec<[u8; 32]>, - Vec, - Vec, - Vec, - Vec, + Txid, + Option, + Vec<(u16, u32, Block, Transaction)>, )> { let (indexed_watchtower_inputs, graph_watchtower_xonly_public_keys) = parse_indexed_watchtower_inputs(watchtower_challenge_txids, watchtower_public_keys)?; + let watchtower_challenge_init_txid = Txid::from_str(watchtower_challenge_init_txid)?; let btc_client = BTCClient::new(bitcoin_network, Some(esplora_url)); let latest_sequencer_commit_txid = Txid::from_str(latest_sequencer_commit_txid)?; @@ -279,48 +273,40 @@ pub async fn fetch_target_block_and_watchtower_tx( } // --- watchtower_challenge_txns --- // - let mut watchtower_challenge_txns = Vec::new(); - let mut watchtower_challenge_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(); - - let watchtower_challlenge_init_txn: Transaction = - match btc_client.get_tx(&watchtower_challenge_init_txid.parse().unwrap()).await? { + let mut watchtower_challenge_witnesses = Vec::new(); + + let watchtower_challenge_init_txn = if indexed_watchtower_inputs.is_empty() { + None + } else { + let transaction = match btc_client.get_tx(&watchtower_challenge_init_txid).await? { Some(tx) => tx, None => anyhow::bail!( "Failed to fetch watchtower challenge init txn: {}", watchtower_challenge_init_txid ), }; + anyhow::ensure!( + transaction.compute_txid() == watchtower_challenge_init_txid, + "Fetched watchtower challenge init transaction has the wrong txid" + ); + Some(transaction) + }; - for (node_index, txid, public_key) in indexed_watchtower_inputs { - tracing::info!("txid: {}, pk: {}", txid, public_key); + for (node_index, txid) in indexed_watchtower_inputs { + tracing::info!("watchtower challenge txid: {txid}"); let txn = match btc_client.get_tx(&txid).await? { Some(tx) => tx, None => anyhow::bail!("Failed to fetch watchtower challenge txn: {}", txid), }; - // get prev outs - // FIXME: update the index - let index = txn.input[0].previous_output.vout as usize; - watchtower_challenge_txn_prev_outs - .push(watchtower_challlenge_init_txn.output[index].clone()); - - 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 - // generate_pay_to_pubkey_taproot_script - let watchtower_challenge_txn_script: ScriptBuf = { - let public_key: XOnlyPublicKey = public_key.into(); - script! { - { public_key } - OP_CHECKSIG - } - .compile() + let status = btc_client.get_tx_status(&txid).await?; + let block_height = status + .block_height + .ok_or_else(|| anyhow::anyhow!("watchtower challenge is not confirmed: {txid}"))?; + let block = btc_client.get_block_by_height(block_height).await?; + if !block.txdata.iter().any(|candidate| candidate.compute_txid() == txid) { + anyhow::bail!("watchtower challenge is missing from its reported block: {txid}"); }; - watchtower_challenge_txn_scripts.push(watchtower_challenge_txn_script); + watchtower_challenge_witnesses.push((node_index, block_height, block, txn)); } Ok(( @@ -328,12 +314,10 @@ pub async fn fetch_target_block_and_watchtower_tx( target_block_ss_commit, operator_committed_blockhash, operator_latest_sequencer_commit_txn, - watchtower_challenge_indices, graph_watchtower_xonly_public_keys, - watchtower_challenge_txns, - watchtower_challenge_txn_prev_outs, - watchtower_challenge_txn_pubkeys, - watchtower_challenge_txn_scripts, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, )) } pub struct OperatorProofBuilder { @@ -388,12 +372,10 @@ 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, - watchtower_challenge_txn_scripts, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, .. } = ctx else { @@ -525,6 +507,27 @@ impl ProofBuilder for OperatorProofBuilder { target_block_ss_commit.clone(), &bitcoin_block_headers, ); + let watchtower_challenges = watchtower_challenge_witnesses + .iter() + .map(|(node_index, block_height, block, transaction)| { + anyhow::ensure!( + bitcoin_block_headers + .get(*block_height as usize) + .map(CircuitBlockHeader::compute_block_hash) + == Some(*block.block_hash().as_byte_array()), + "watchtower challenge block height is not in the authenticated header archive" + ); + Ok(IndexedWatchtowerChallenge { + node_index: *node_index, + spv: build_spv( + transaction, + *block_height, + block.clone(), + &bitcoin_block_headers, + ), + }) + }) + .collect::>>()?; // Generate the proofs let (proof, cycles, proving_time) = tracing::info_span!("generate proof").in_scope( @@ -537,14 +540,11 @@ impl ProofBuilder for OperatorProofBuilder { stdin.write(&graph_id); stdin.write(&operator_genesis_sequencer_commit_txid.to_byte_array()); - stdin.write(&operator_latest_sequencer_commit_txn); - stdin.write(&watchtower_challenge_indices); + stdin.write(&watchtower_challenge_init_txid.to_byte_array()); + stdin.write(&watchtower_challenge_init_txn); stdin.write(&graph_watchtower_xonly_public_keys); - stdin.write(&watchtower_challenge_txns); - stdin.write(&watchtower_challenge_txn_pubkeys); - stdin.write(&watchtower_challenge_txn_scripts); - stdin.write(&watchtower_challenge_txn_prev_outs); + stdin.write(&watchtower_challenges); stdin.write(&header_chain_input); stdin.write(&commit_chain_input); @@ -604,9 +604,8 @@ mod tests { use ark_bn254::Bn254; use ark_groth16::{Groth16, r1cs_to_qap::LibsnarkReduction}; - use std::panic::{AssertUnwindSafe, catch_unwind}; - use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk}; + use zkm_verifier::{IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk}; #[tokio::test] #[ignore = "local test"] @@ -629,13 +628,7 @@ mod tests { U256::from_le_bytes(a.included_watchtowers) ); - let part_stark_vk = catch_unwind(AssertUnwindSafe(|| { - Groth16Verifier::get_part_stark_vk(&proof.zkm_version) - })) - .map_err(|_| { - anyhow::anyhow!("Failed to load part_stark_vk for zkm_version {}", proof.zkm_version) - }) - .unwrap(); + let part_stark_vk = zkm_verifier::Groth16Verifier::get_part_stark_vk(&proof.zkm_version); let ark_proof = convert_ark_imm_wrap_vk(&proof, &vk_hash, &IMM_GROTH16_VK_BYTES, part_stark_vk) .unwrap(); diff --git a/circuits/operator-proof/host/src/main.rs b/circuits/operator-proof/host/src/main.rs index 95a2ddc35..dab623f7b 100644 --- a/circuits/operator-proof/host/src/main.rs +++ b/circuits/operator-proof/host/src/main.rs @@ -24,12 +24,10 @@ 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, - watchtower_challenge_txn_scripts, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, ) = fetch_target_block_and_watchtower_tx( &args.esplora_url, &args.latest_sequencer_commit_txid, @@ -59,12 +57,10 @@ 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, - watchtower_challenge_txn_scripts, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, }; let (input, proof, cycles, _) = builder.build_proof(&ctx).unwrap(); tracing::info!("Operator proof cycles: {cycles}"); diff --git a/circuits/proof-builder/src/lib.rs b/circuits/proof-builder/src/lib.rs index 528fce114..2d7ee3023 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -1,5 +1,5 @@ use anyhow::Result; -use bitcoin::{Block, BlockHash, ScriptBuf, Transaction, TxOut}; +use bitcoin::{Block, BlockHash, Transaction, Txid}; use commit_chain::CircuitCommit; use header_chain::CircuitBlockHeader; use serde::{Deserialize, Serialize}; @@ -63,12 +63,10 @@ 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, - watchtower_challenge_txn_scripts: Vec, + watchtower_challenge_init_txid: Txid, + watchtower_challenge_init_txn: Option, + watchtower_challenge_witnesses: Vec<(u16, u32, Block, Transaction)>, }, } diff --git a/crates/bitcoin-light-client-circuit/src/lib.rs b/crates/bitcoin-light-client-circuit/src/lib.rs index dfa5703a1..7f58c96f3 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -8,7 +8,8 @@ use alloy_primitives::U256; use bitcoin::Block; use bitcoin::hashes::{Hash, HashEngine, sha256}; use commit_chain::{ - CommitChainCircuitInput, commit_chain_commitment_digest, decode_commit_chain_circuit_output, + AuthorizedProgramIds, CommitChainCircuitInput, CommitChainCircuitOutput, + commit_chain_commitment_digest, decode_commit_chain_circuit_output, extract_commit_chain_commitment, extract_data_from_commitment_outputs, sequencer_hash, }; use header_chain::{ @@ -18,28 +19,11 @@ use header_chain::{ use state_chain::{StateChainCircuitInput, StateChainCircuitOutput, verify_sequencer_commit}; use zkm_primitives::io::ZKMPublicValues; -use bitcoin::{ - ScriptBuf, Transaction, TxOut, - secp256k1::{PublicKey, XOnlyPublicKey}, -}; +use bitcoin::{Transaction, secp256k1::XOnlyPublicKey}; pub use guest_executor::io::EthClientExecutorInput; use serde::{Deserialize, Serialize}; use verifier::verify_groth16_proof; -// Regenerate these IDs after changing a leaf guest or the verifier. -// TODO: Generated by `proof-builder-rpc` -pub const EXPECTED_HEADER_CHAIN_PROGRAM_ID: verifier::ProgramId = [ - 0x87, 0x6b, 0xd8, 0x4c, 0xc8, 0x9b, 0xa4, 0x57, 0xa0, 0xfb, 0x94, 0xfc, 0x20, 0x13, 0xbd, 0x75, - 0x66, 0xae, 0xcc, 0x1e, 0x04, 0xbe, 0xfa, 0x53, 0x6c, 0x0c, 0x38, 0xd9, 0x57, 0x48, 0xf4, 0xf4, -]; -pub const EXPECTED_STATE_CHAIN_PROGRAM_ID: verifier::ProgramId = [ - 0x73, 0xc0, 0x74, 0x70, 0xf9, 0x7b, 0x3f, 0x8d, 0xe7, 0x48, 0x94, 0xad, 0x1a, 0xec, 0x60, 0xfb, - 0x82, 0x2c, 0x67, 0xf2, 0x97, 0x3a, 0x6f, 0xda, 0xef, 0x9f, 0x2b, 0xa6, 0xab, 0xb7, 0x3a, 0x17, -]; -pub const EXPECTED_COMMIT_CHAIN_PROGRAM_ID: verifier::ProgramId = [ - 0x99, 0xda, 0x42, 0x82, 0x30, 0x8f, 0x07, 0x42, 0x11, 0xd9, 0x59, 0x30, 0xd5, 0x26, 0x2c, 0xc2, - 0x4e, 0xd9, 0x13, 0x89, 0x79, 0xdf, 0x2e, 0x83, 0x59, 0xea, 0x6e, 0xc6, 0x8b, 0x91, 0xe3, 0x7d, -]; pub const GRAPH_ID_SIZE: usize = 16; pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; @@ -62,19 +46,86 @@ pub struct OperatorPublicOutputs { pub included_watchtowers: [u8; 32], } +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct IndexedWatchtowerChallenge { + pub node_index: u16, + pub spv: SPV, +} + +/// Verifies that a proof identity matches both its output and Publisher authorization. +fn check_program_id( + actual_program_id: verifier::ProgramId, + output_program_id: verifier::ProgramId, + authorized_program_id: verifier::ProgramId, +) { + assert_eq!(actual_program_id, authorized_program_id, "unauthorized proof program id"); + assert_eq!(output_program_id, actual_program_id, "proof output program id mismatch"); +} + fn checked_history_root( program_type: verifier::ProgramType, actual_program_id: verifier::ProgramId, output_program_id: verifier::ProgramId, - expected_program_id: verifier::ProgramId, + authorized_program_id: verifier::ProgramId, history: [u8; 32], ) -> [u8; 32] { - assert_eq!(actual_program_id, expected_program_id, "unexpected proof program id"); - assert_eq!(output_program_id, actual_program_id, "proof output program id mismatch"); - + check_program_id(actual_program_id, output_program_id, authorized_program_id); verifier::finalize_history(program_type, history, actual_program_id) } +/// Verifies that the latest Bitcoin commitment authorizes every supplied circuit proof. +fn verify_commitment_authorization( + commit_chain_output: &CommitChainCircuitOutput, + header_chain_output: &BlockHeaderCircuitOutput, + state_chain_output: &StateChainCircuitOutput, + header_program_id: verifier::ProgramId, + state_program_id: verifier::ProgramId, + commit_program_id: verifier::ProgramId, + sequencer_set_hash: [u8; 32], +) -> AuthorizedProgramIds { + let authorized = commit_chain_output.chain_state.authorized_program_ids; + authorized.validate().expect("invalid authorized ProgramIds"); + check_program_id(commit_program_id, commit_chain_output.self_program_id, authorized.commit); + let program_history_root = verifier::program_history_root( + checked_history_root( + verifier::ProgramType::Header, + header_program_id, + header_chain_output.self_program_id, + authorized.header, + header_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::State, + state_program_id, + state_chain_output.self_program_id, + authorized.state, + state_chain_output.program_history_hash, + ), + ); + let checkpoint_root = verifier::proof_checkpoint_root( + header_chain_output.upgrade_checkpoint_hash, + state_chain_output.upgrade_checkpoint_hash, + ); + assert_eq!( + checkpoint_root, commit_chain_output.chain_state.proof_checkpoint_root, + "proof checkpoint root mismatch" + ); + let commitment = + extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) + .expect("invalid commit-chain commitment output"); + assert_eq!( + commitment, + commit_chain_commitment_digest( + sequencer_set_hash, + state_chain_output.chain_state.genesis_evm_block_hash, + program_history_root, + checkpoint_root, + authorized, + ) + ); + authorized +} + pub fn decode_operator_public_outputs( public_values: &[u8], ) -> Result { @@ -127,9 +178,14 @@ pub fn watch_longest_chain( let btc_header_chain_output: BlockHeaderCircuitOutput = ZKMPublicValues::from(&header_chain.zkm_public_values).read(); - // verify that the latest_sequecner_commit_tx is in the header chain - println!("SPV"); - assert!(spv.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); + assert_eq!( + btc_header_chain_output.chain_state.block_hashes_mmr.size, + btc_header_chain_output.chain_state.block_height + 1, + "header MMR size mismatch" + ); + let commitment_block_height = spv + .verify(&btc_header_chain_output.chain_state.block_hashes_mmr) + .expect("sequencer commitment SPV verification failed"); let state_program_id = verify_groth16_proof( &state_chain.zkm_proof, @@ -151,40 +207,15 @@ pub fn watch_longest_chain( let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); - let commitment = - extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) - .expect("invalid commit-chain commitment output"); - let program_history_root = verifier::program_history_root( - checked_history_root( - verifier::ProgramType::Header, + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + verify_commitment_authorization( + &commit_chain_output, + &btc_header_chain_output, + &state_chain_output, header_program_id, - btc_header_chain_output.self_program_id, - EXPECTED_HEADER_CHAIN_PROGRAM_ID, - btc_header_chain_output.program_history_hash, - ), - checked_history_root( - verifier::ProgramType::State, state_program_id, - state_chain_output.self_program_id, - EXPECTED_STATE_CHAIN_PROGRAM_ID, - state_chain_output.program_history_hash, - ), - checked_history_root( - verifier::ProgramType::Commit, commit_program_id, - commit_chain_output.self_program_id, - EXPECTED_COMMIT_CHAIN_PROGRAM_ID, - commit_chain_output.program_history_hash, - ), - ); - if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { - assert_eq!( - commitment, - commit_chain_commitment_digest( - sequencer_set_hash, - state_chain_output.chain_state.genesis_evm_block_hash, - program_history_root, - ) + sequencer_set_hash, ); } else { panic!("Invalid commitment: inconsistent sequencer set hash"); @@ -192,7 +223,7 @@ pub fn watch_longest_chain( println!("commit public inputs"); // commit public inputs - (btc_header_chain_output.chain_state.total_work, commit_chain_output.chain_state.block_height) + (btc_header_chain_output.chain_state.total_work, commitment_block_height) } pub fn u256_to_le_bits(u: U256) -> [bool; 256] { @@ -213,40 +244,15 @@ pub fn le_bits_to_u256(bits: &[bool]) -> U256 { u } -pub fn verify_graph_watchtower_pubkey( - graph_watchtower_xonly_public_keys: &[[u8; 32]], - index: usize, - pubkey: &PublicKey, -) -> Result<(), String> { - // 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(); - if &actual != expected { - return Err(format!( - "watchtower[{index}] pubkey mismatch: actual={}, expected={}", - hex::encode(actual), - hex::encode(expected) - )); - } - 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 { @@ -265,6 +271,28 @@ pub fn validate_watchtower_challenge_indices( Ok(()) } +/// Checks an optional challenge-init transaction against its graph txid. +/// A transaction is required when `challenges_present` is true. +fn checked_challenge_init_transaction( + expected_txid: [u8; 32], + transaction: Option<&Transaction>, + challenges_present: bool, +) -> Option<&Transaction> { + if let Some(transaction) = transaction { + assert_eq!( + transaction.compute_txid().to_byte_array(), + expected_txid, + "watchtower challenge init transaction txid mismatch" + ); + } + assert!( + !challenges_present || transaction.is_some(), + "watchtower challenge init transaction is required when challenges exist" + ); + + transaction +} + // calculate operator public input: https://github.com/ProjectZKM/Ziren/blob/main/crates/sdk/src/utils.rs#L42 #[allow(clippy::too_many_arguments)] pub fn propose_longest_chain( @@ -272,13 +300,10 @@ 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, + watchtower_challenge_init_txid: [u8; 32], + watchtower_challenge_init_txn: Option, + watchtower_challenges: Vec, graph_watchtower_xonly_public_keys: &[[u8; 32]], - expected_watchtower_program_id: verifier::ProgramId, operator_header_chain: HeaderChainCircuitInput, commit_chain: CommitChainCircuitInput, @@ -317,145 +342,15 @@ pub fn propose_longest_chain( let btc_header_chain_output: BlockHeaderCircuitOutput = ZKMPublicValues::from(&operator_header_chain.zkm_public_values).read(); let operator_total_work = btc_header_chain_output.chain_state.total_work; - let operator_consensus_block_height = U32::from(commit_chain_output.chain_state.block_height); - // commit header chain best block hash as pis let btc_best_block_hash = btc_header_chain_output.chain_state.best_block_hash; - - // verify that the latest_sequecner_commit_tx is in the header chain - assert!(spv_ss_commit.verify(&btc_header_chain_output.chain_state.block_hashes_mmr)); - - // parse included_watchtowers into bits array - let included_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"); - - // 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 (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[challenge_position]; - println!("Verify watchtower[{i}] tx: {}, {:?}", tx.compute_txid(), tx); - 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[challenge_position]; - - if let Err(err) = - verify_graph_watchtower_pubkey(graph_watchtower_xonly_public_keys, i, pubkey) - { - println!("Watchtower[{i}] graph 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[challenge_position], - tx, - prev_index, - prev_out, - pubkey, - &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(program_id) if program_id == expected_watchtower_program_id => {} - Ok(_) => { - println!("Watchtower[{i}] unexpected program id"); - continue; - } - Err(err) => { - println!("Watchtower[{i}] invalid proof: {err}"); - continue; - } - } - - 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_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; - } - } - } + assert_eq!( + btc_header_chain_output.chain_state.block_hashes_mmr.size, + btc_header_chain_output.chain_state.block_height + 1, + "header MMR size mismatch" + ); + let operator_consensus_block_height = spv_ss_commit + .verify(&btc_header_chain_output.chain_state.block_hashes_mmr) + .expect("sequencer commitment SPV verification failed"); println!("verify el block"); let state_program_id = verify_groth16_proof( @@ -478,46 +373,99 @@ pub fn propose_longest_chain( let commit_sequencer_set_hash = sequencer_hash(&commit_chain_output.chain_state.sequencers); let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; - let commitment = - extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) - .expect("invalid commit-chain commitment output"); - let program_history_root = verifier::program_history_root( - checked_history_root( - verifier::ProgramType::Header, - header_program_id, - btc_header_chain_output.self_program_id, - EXPECTED_HEADER_CHAIN_PROGRAM_ID, - btc_header_chain_output.program_history_hash, - ), - checked_history_root( - verifier::ProgramType::State, - state_program_id, - state_chain_output.self_program_id, - EXPECTED_STATE_CHAIN_PROGRAM_ID, - state_chain_output.program_history_hash, - ), - checked_history_root( - verifier::ProgramType::Commit, - commit_program_id, - commit_chain_output.self_program_id, - EXPECTED_COMMIT_CHAIN_PROGRAM_ID, - commit_chain_output.program_history_hash, - ), - ); - if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { - assert_eq!( - commitment, - commit_chain_commitment_digest( + let authorized = + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + verify_commitment_authorization( + &commit_chain_output, + &btc_header_chain_output, + &state_chain_output, + header_program_id, + state_program_id, + commit_program_id, sequencer_set_hash, - state_chain_output.chain_state.genesis_evm_block_hash, - program_history_root, ) + } else { + panic!("Invalid commitment: inconsistent sequencer set hash"); + }; + assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); + + let included_watchtowers_bits = u256_to_le_bits(included_watchtowers); + let challenge_indices = + watchtower_challenges.iter().map(|challenge| challenge.node_index).collect::>(); + validate_watchtower_challenge_indices( + &included_watchtowers_bits, + graph_watchtower_xonly_public_keys.len(), + &challenge_indices, + ) + .expect("invalid indexed watchtower challenges"); + + let watchtower_challenge_init_txn = checked_challenge_init_transaction( + watchtower_challenge_init_txid, + watchtower_challenge_init_txn.as_ref(), + !watchtower_challenges.is_empty(), + ); + for challenge in &watchtower_challenges { + let i = challenge.node_index as usize; + let challenge_height = challenge + .spv + .verify(&btc_header_chain_output.chain_state.block_hashes_mmr) + .expect("watchtower challenge SPV verification failed"); + assert!( + challenge_height <= btc_header_chain_output.chain_state.block_height, + "challenge height exceeds authenticated header chain" ); - } else { - panic!("Invalid commitment: inconsistent sequencer set hash"); - }; - assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); + let tx = &challenge.spv.transaction.0; + let input = tx.input.first().expect("watchtower challenge must have input 0"); + let expected_vout = u32::from(challenge.node_index) * 2; + assert_eq!(input.previous_output.txid.to_byte_array(), watchtower_challenge_init_txid); + assert_eq!(input.previous_output.vout, expected_vout); + + let prev_out = watchtower_challenge_init_txn + .expect("watchtower challenge init transaction is required") + .output + .get(expected_vout as usize) + .expect("watchtower challenge prevout is missing"); + let xonly = XOnlyPublicKey::from_slice(&graph_watchtower_xonly_public_keys[i]) + .expect("invalid graph watchtower x-only key"); + let script = bitcoin::blockdata::script::Builder::new() + .push_x_only_key(&xonly) + .push_opcode(bitcoin::opcodes::all::OP_CHECKSIG) + .into_script(); + verify_taproot_leaf_schnorr_signature(&script, tx, 0, prev_out, &xonly) + .expect("watchtower challenge signature verification failed"); + + let Ok(commitment) = extract_data_from_commitment_outputs(&tx.output) else { + continue; + }; + let Ok(( + parsed_graph_id, + proof, + public_values, + vk, + watchtower_total_work, + watchtower_consensus_block_height, + zkm_version, + )) = parse_watchtower_commitment(&commitment) + else { + continue; + }; + let Ok(program_id) = verify_groth16_proof(&proof, &public_values, &vk, &zkm_version) else { + continue; + }; + if program_id != authorized.watchtower || parsed_graph_id != graph_id { + continue; + } + assert!( + U256::from_be_bytes(watchtower_total_work) <= U256::from_be_bytes(operator_total_work), + "valid watchtower challenge has more work than operator" + ); + assert!( + u32::from_le_bytes(watchtower_consensus_block_height) + <= operator_consensus_block_height, + "valid watchtower challenge has a later commitment than operator" + ); + } let mut is_found = false; for withdrawal in &state_chain_output.chain_state.withdrawals { @@ -538,6 +486,7 @@ pub fn propose_longest_chain( let constant = hash_operator_constant( graph_id, operator_genesis_sequencer_commit_txid, + watchtower_challenge_init_txid, graph_watchtower_xonly_public_keys, ); println!("constant hex: {:?}", hex::encode(constant)); @@ -561,12 +510,14 @@ pub fn propose_longest_chain( pub fn hash_operator_constant( graph_id: [u8; GRAPH_ID_SIZE], operator_genesis_sequencer_commit_txid: [u8; 32], + watchtower_challenge_init_txid: [u8; 32], watchtower_xonly_public_keys: &[[u8; 32]], ) -> [u8; 32] { let mut engine = sha256::HashEngine::default(); - engine.input(b"bitvm2/operator-constant/v2"); + engine.input(b"bitvm/operator-constant/v3"); engine.input(&graph_id); engine.input(&operator_genesis_sequencer_commit_txid); + engine.input(&watchtower_challenge_init_txid); engine.input(&(watchtower_xonly_public_keys.len() as u16).to_be_bytes()); for key in watchtower_xonly_public_keys { engine.input(key); @@ -801,6 +752,21 @@ mod tests { assert!(wrong_output.is_err()); } + #[test] + fn check_program_id_rejects_unauthorized_or_mismatched_outputs() { + let program_id = [1u8; 32]; + check_program_id(program_id, program_id, program_id); + + assert!( + std::panic::catch_unwind(|| check_program_id(program_id, program_id, [2u8; 32])) + .is_err() + ); + assert!( + std::panic::catch_unwind(|| check_program_id(program_id, [2u8; 32], program_id)) + .is_err() + ); + } + #[test] fn test_build_watchtower_commitment() { let graph_id = hex::decode("00112233445566778899aabbccddeeff").unwrap().try_into().unwrap(); @@ -868,9 +834,11 @@ mod tests { 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(); + let challenge_init_txid = [5u8; 32]; + let mut input = b"bitvm/operator-constant/v3".to_vec(); input.extend_from_slice(&graph_id); input.extend_from_slice(&genesis_txid); + input.extend_from_slice(&challenge_init_txid); input.extend_from_slice(&(watchtower_keys.len() as u16).to_be_bytes()); for key in &watchtower_keys { input.extend_from_slice(key); @@ -878,28 +846,53 @@ mod tests { let expected = bitcoin::hashes::sha256::Hash::hash(&input); assert_eq!( - hash_operator_constant(graph_id, genesis_txid, &watchtower_keys), + hash_operator_constant(graph_id, genesis_txid, challenge_init_txid, &watchtower_keys), *expected.as_byte_array() ); assert_ne!( hash_operator_constant( graph_id, genesis_txid, + challenge_init_txid, &[watchtower_keys[1], watchtower_keys[0]], ), *expected.as_byte_array() ); } + #[test] + fn optional_challenge_init_transaction_is_fail_closed() { + let transaction = Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + let txid = transaction.compute_txid().to_byte_array(); + + assert!(checked_challenge_init_transaction(txid, None, false).is_none()); + assert!(checked_challenge_init_transaction(txid, Some(&transaction), true).is_some()); + assert!( + std::panic::catch_unwind(|| { + checked_challenge_init_transaction([1u8; 32], Some(&transaction), false) + }) + .is_err() + ); + assert!( + std::panic::catch_unwind(|| { checked_challenge_init_transaction(txid, None, true) }) + .is_err() + ); + } + #[test] fn watchtower_challenge_indices_preserve_sparse_bitmap_positions() { let mut included = [false; 256]; included[1] = true; included[4] = true; - validate_watchtower_challenge_indices(&included, 5, &[1, 4], 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()); + validate_watchtower_challenge_indices(&included, 5, &[1, 4]).unwrap(); + assert!(validate_watchtower_challenge_indices(&included, 5, &[0, 1]).is_err()); + assert!(validate_watchtower_challenge_indices(&included, 5, &[1, 1]).is_err()); } #[test] @@ -930,7 +923,7 @@ mod tests { ).unwrap(); let tx: Transaction = deserialize(&bytes).unwrap(); - let commitment = extract_data_from_commitment_outputs(&tx.output); + let commitment = extract_data_from_commitment_outputs(&tx.output).unwrap(); let parse_result = parse_watchtower_commitment(&commitment); assert!(parse_result.is_err(), "legacy commitment with trailing zkm_version should fail"); } diff --git a/crates/bitcoin-light-client-circuit/src/signature.rs b/crates/bitcoin-light-client-circuit/src/signature.rs index 49510880a..c789f696f 100644 --- a/crates/bitcoin-light-client-circuit/src/signature.rs +++ b/crates/bitcoin-light-client-circuit/src/signature.rs @@ -7,9 +7,9 @@ pub use tendermint_light_client_verifier::{ use bitcoin::{ Script, ScriptBuf, Transaction, TxOut, key::Keypair, - secp256k1::{Message as EcdsaMessage, PublicKey, Secp256k1, XOnlyPublicKey}, + secp256k1::{Message as EcdsaMessage, Secp256k1, XOnlyPublicKey}, sighash::{Prevouts, SighashCache, TapSighashType}, - taproot::{LeafVersion, Signature as TaprootSignature, TapLeafHash}, + taproot::{ControlBlock, LeafVersion, Signature as TaprootSignature, TapLeafHash}, }; /// Generate Taproot script-path's Schnorr signature @@ -40,26 +40,47 @@ fn generate_taproot_leaf_schnorr_signature( TaprootSignature { signature: sig, sighash_type } } -/// Verify Schnorr signature -/// +/// Verifies the Taproot script-path witness and Schnorr signature for one transaction input. pub fn verify_taproot_leaf_schnorr_signature( script: &ScriptBuf, spending_tx: &Transaction, - prev_index: usize, + input_index: usize, prev_out: &TxOut, - pubkey: &PublicKey, - sig: &TaprootSignature, + pubkey: &XOnlyPublicKey, ) -> Result<(), Box> { + let input = spending_tx.input.get(input_index).ok_or("Invalid input index")?; + let sig = input.witness.iter().next().ok_or("Missing Taproot signature").and_then(|bytes| { + TaprootSignature::from_slice(bytes).map_err(|_| "Invalid Taproot signature") + })?; if sig.sighash_type != TapSighashType::AllPlusAnyoneCanPay { return Err("Invalid sig type".into()); } let secp = Secp256k1::verification_only(); + let witness_len = input.witness.len(); + if witness_len < 3 { + return Err("Invalid Taproot script-path witness".into()); + } + let witness_script = + input.witness.iter().nth(witness_len - 2).ok_or("Missing Taproot witness script")?; + if witness_script != script.as_bytes() { + return Err("Taproot witness script mismatch".into()); + } + let control_block = ControlBlock::decode( + input.witness.iter().nth(witness_len - 1).ok_or("Missing Taproot control block")?, + )?; + let script_pubkey = prev_out.script_pubkey.as_bytes(); + if script_pubkey.len() != 34 || script_pubkey[0] != 0x51 || script_pubkey[1] != 0x20 { + return Err("Prevout is not P2TR".into()); + } + let output_key = XOnlyPublicKey::from_slice(&script_pubkey[2..])?; + if !control_block.verify_taproot_commitment(&secp, output_key, script) { + return Err("Taproot control block does not commit to the witness script".into()); + } + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); - let internal_xonly: XOnlyPublicKey = (*pubkey).into(); let sighash = match SighashCache::new(spending_tx).taproot_script_spend_signature_hash( - 0, - //&Prevouts::All(&[prev_out.clone()]), - &Prevouts::One(prev_index, prev_out.clone()), + input_index, + &Prevouts::One(input_index, prev_out.clone()), leaf_hash, TapSighashType::AllPlusAnyoneCanPay, ) { @@ -68,7 +89,7 @@ pub fn verify_taproot_leaf_schnorr_signature( }; let msg = EcdsaMessage::from(sighash); - Ok(secp.verify_schnorr(&sig.signature, &msg, &internal_xonly)?) + Ok(secp.verify_schnorr(&sig.signature, &msg, pubkey)?) } #[cfg(test)] @@ -141,25 +162,44 @@ mod tests { &keypair, ); - // 7. Verify the signature - verify_taproot_leaf_schnorr_signature( - &script, - &spending_tx, - 0, - &prev_out, - &keypair.public_key(), - &sig, - ) - .unwrap(); - println!("Schnorr signature verified successfully!"); - - // 8. Construct control block + witness + // 7. Construct control block + witness let control_block = taproot_info .control_block(&(script.clone(), LeafVersion::TapScript)) .expect("control block"); - spending_tx.input[0].witness = - Witness::from(vec![sig.to_vec(), script.into_bytes(), control_block.serialize()]); + spending_tx.input[0].witness = Witness::from(vec![ + sig.to_vec(), + script.clone().into_bytes(), + control_block.serialize(), + ]); + + // 8. Verify the signature and Taproot script commitment. + verify_taproot_leaf_schnorr_signature(&script, &spending_tx, 0, &prev_out, &internal_xonly) + .unwrap(); + let mut wrong_prevout = prev_out.clone(); + wrong_prevout.script_pubkey = ScriptBuf::new(); + assert!( + verify_taproot_leaf_schnorr_signature( + &script, + &spending_tx, + 0, + &wrong_prevout, + &internal_xonly, + ) + .is_err() + ); + let mut missing_signature = spending_tx.clone(); + missing_signature.input[0].witness = Witness::new(); + assert!( + verify_taproot_leaf_schnorr_signature( + &script, + &missing_signature, + 0, + &prev_out, + &internal_xonly, + ) + .is_err() + ); println!("Final spending tx hex = {}", hex::encode(serialize(&spending_tx))); } diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index f14c25d2a..5af821ecc 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -13,6 +13,24 @@ use bincode::Options as BincodeOptions; use bitcoin::{Transaction, TxOut, Witness, hashes::Hash as _, secp256k1::PublicKey}; use sha2::{Digest, Sha256}; +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Copy, Default)] +pub struct AuthorizedProgramIds { + pub header: verifier::ProgramId, + pub state: verifier::ProgramId, + pub commit: verifier::ProgramId, + pub watchtower: verifier::ProgramId, +} + +impl AuthorizedProgramIds { + /// Rejects an authorization set containing an unconfigured ProgramId. + pub fn validate(&self) -> Result<(), String> { + if [self.header, self.state, self.commit, self.watchtower].contains(&[0u8; 32]) { + return Err("authorized ProgramIds must be non-zero".to_string()); + } + Ok(()) + } +} + #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct CommitInfo { pub threshold: u16, @@ -26,10 +44,11 @@ pub struct CommitInfo { pub sequencers: Vec, pub genesis_evm_block_hash: [u8; 32], pub program_history_root: [u8; 32], + pub proof_checkpoint_root: [u8; 32], + pub authorized_program_ids: AuthorizedProgramIds, } -/// The input proof of the commit chain circuit. -/// The proof can be either None (implying the beginning) or a Succinct proof. +/// Selects a Genesis replay or a same-Program recursive predecessor. #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub enum CommitChainPrevProofType { GenesisBlock, @@ -50,6 +69,8 @@ pub struct CircuitCommit { pub genesis_evm_block_hash: [u8; 32], pub program_history_root: [u8; 32], pub block_height: u32, // Bitcoin block height of current commitment + pub proof_checkpoint_root: [u8; 32], + pub authorized_program_ids: AuthorizedProgramIds, } impl CommitInfo { @@ -105,6 +126,8 @@ pub struct CommitChainState { pub sequencers: Vec, pub publisher_public_keys: Vec, pub threshold: u16, + pub proof_checkpoint_root: [u8; 32], + pub authorized_program_ids: AuthorizedProgramIds, } impl CircuitCommit { @@ -121,13 +144,12 @@ pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; pub const VK_HASH_SIZE: usize = 66; pub const COMMIT_CHAIN_COMMITMENT_SIZE: usize = 32; -const COMMIT_CHAIN_COMMITMENT_DOMAIN: &[u8] = b"bitvm2/commit-chain/v1"; +const COMMIT_CHAIN_COMMITMENT_DOMAIN: &[u8] = b"bitvm/commit-chain/v2"; #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct CommitChainCircuitOutput { pub chain_state: CommitChainState, pub self_program_id: verifier::ProgramId, - pub program_history_hash: [u8; 32], } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -165,17 +187,28 @@ pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> Hash { sequencer_set.hash() } +/// Hashes the Publisher commitment, proof checkpoints, and authorized circuit identities. pub fn commit_chain_commitment_digest( sequencer_set_hash: [u8; 32], genesis_evm_block_hash: [u8; 32], program_history_root: [u8; 32], + proof_checkpoint_root: [u8; 32], + authorized_program_ids: AuthorizedProgramIds, ) -> [u8; 32] { assert_ne!(program_history_root, [0u8; 32], "program history root must be non-zero"); + assert_ne!(proof_checkpoint_root, [0u8; 32], "proof checkpoint root must be non-zero"); + authorized_program_ids.validate().expect("invalid authorized ProgramIds"); + let mut hasher = Sha256::new(); hasher.update(COMMIT_CHAIN_COMMITMENT_DOMAIN); hasher.update(sequencer_set_hash); hasher.update(genesis_evm_block_hash); hasher.update(program_history_root); + hasher.update(proof_checkpoint_root); + hasher.update(authorized_program_ids.header); + hasher.update(authorized_program_ids.state); + hasher.update(authorized_program_ids.commit); + hasher.update(authorized_program_ids.watchtower); hasher.finalize().into() } @@ -230,6 +263,8 @@ impl CommitChainState { sequencers: Vec::new(), publisher_public_keys: vec![], threshold: u16::MAX, + proof_checkpoint_root: [0u8; 32], + authorized_program_ids: AuthorizedProgramIds::default(), } } @@ -252,14 +287,14 @@ impl CommitChainState { let actual_commitment = extract_commit_chain_commitment(&latest_commit_txn_with_wtns.output).unwrap(); if let Hash::Sha256(latest_sequencer_set_hash) = sequencer_hash(latest_sequencers) { - assert_eq!( - actual_commitment, - commit_chain_commitment_digest( - latest_sequencer_set_hash, - commit.genesis_evm_block_hash, - commit.program_history_root, - ) + let expected_commitment = commit_chain_commitment_digest( + latest_sequencer_set_hash, + commit.genesis_evm_block_hash, + commit.program_history_root, + commit.proof_checkpoint_root, + commit.authorized_program_ids, ); + assert_eq!(actual_commitment, expected_commitment); } else { panic!("Invalid latest sequencer set hash"); } @@ -312,25 +347,35 @@ impl CommitChainState { self.publisher_public_keys = next_publisher_public_keys.to_vec(); self.threshold = next_threshold; self.block_height = commit.block_height; + self.proof_checkpoint_root = commit.proof_checkpoint_root; + self.authorized_program_ids = commit.authorized_program_ids; } } } -pub fn extract_data_from_commitment_outputs(txouts: &[TxOut]) -> Vec { +/// Reassembles pushed commitment chunks through the terminating OP_RETURN output. +pub fn extract_data_from_commitment_outputs(txouts: &[TxOut]) -> Result, String> { let mut data = vec![]; for txout in txouts { let script = &txout.script_pubkey; - let instructions = script.instructions_minimal().collect::, _>>().unwrap(); - if let bitcoin::blockdata::script::Instruction::PushBytes(bytes) = &instructions[1] { - data.extend_from_slice(bytes.as_bytes()); - } - if let bitcoin::script::Instruction::Op(op) = instructions[0] - && op == bitcoin::opcodes::all::OP_RETURN - { - break; + let instructions = script + .instructions_minimal() + .collect::, _>>() + .map_err(|err| format!("invalid challenge commitment script: {err}"))?; + let Some(bitcoin::blockdata::script::Instruction::PushBytes(bytes)) = instructions.get(1) + else { + return Err("challenge commitment output must contain pushed bytes".to_string()); + }; + data.extend_from_slice(bytes.as_bytes()); + if matches!( + instructions.first(), + Some(bitcoin::script::Instruction::Op(op)) + if *op == bitcoin::opcodes::all::OP_RETURN + ) { + return Ok(data); } } - data + Err("challenge commitment is missing OP_RETURN terminator".to_string()) } #[cfg(test)] @@ -366,31 +411,66 @@ mod tests { sequencers: &[SequencerInfo], genesis_evm_block_hash: [u8; 32], program_history_root: [u8; 32], + proof_checkpoint_root: [u8; 32], + authorized_program_ids: AuthorizedProgramIds, ) -> PushBytesBuf { - let sequencer_set_hash = - if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = - sequencer_hash(sequencers) - { - hash - } else { - panic!("expected sha256 sequencer hash"); - }; + let Hash::Sha256(sequencer_set_hash) = sequencer_hash(sequencers) else { + panic!("expected sha256 sequencer hash"); + }; PushBytesBuf::try_from( commit_chain_commitment_digest( sequencer_set_hash, genesis_evm_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, ) .to_vec(), ) .expect("commitment payload is pushable") } + fn authorized_program_ids() -> AuthorizedProgramIds { + AuthorizedProgramIds { + header: [1; 32], + state: [2; 32], + commit: [3; 32], + watchtower: [4; 32], + } + } + #[test] fn test_commit_chain_commitment_digest_known_vector() { assert_eq!( - hex::encode(commit_chain_commitment_digest([0x11; 32], [0x22; 32], [0x33; 32],)), - "28c3b9bb2c89dcc9867e43e457c5232f229b340823c34a84dc6ed0e8f88147e9" + hex::encode(commit_chain_commitment_digest( + [0x11; 32], + [0x22; 32], + [0x33; 32], + [0x44; 32], + authorized_program_ids(), + )), + "ca5b93b9c16288dd057cfb6c6123d14d2c7a608971724239dc0714c2f5774e36" + ); + } + + #[test] + fn commitment_binds_checkpoints_and_all_program_ids() { + let ids = authorized_program_ids(); + let digest = commit_chain_commitment_digest([6; 32], [7; 32], [8; 32], [9; 32], ids); + + assert_ne!( + digest, + commit_chain_commitment_digest([6; 32], [7; 32], [8; 32], [10; 32], ids) + ); + assert_ne!( + digest, + commit_chain_commitment_digest( + [6; 32], + [7; 32], + [8; 32], + [9; 32], + AuthorizedProgramIds { watchtower: [11; 32], ..ids }, + ) ); } @@ -423,7 +503,13 @@ mod tests { #[test] #[should_panic(expected = "program history root must be non-zero")] fn test_commit_chain_commitment_digest_rejects_zero_program_history_root() { - commit_chain_commitment_digest([0x11; 32], [0x22; 32], [0; 32]); + commit_chain_commitment_digest( + [0x11; 32], + [0x22; 32], + [0; 32], + [0x44; 32], + authorized_program_ids(), + ); } #[test] @@ -478,7 +564,6 @@ mod tests { }; let public_values = bincode::serialize(&old_output).unwrap(); - assert!(bincode::deserialize::(&public_values).is_ok()); assert!(classify_commit_chain_output(&public_values).is_err()); assert!( std::panic::catch_unwind(|| decode_commit_chain_circuit_output(&public_values)) @@ -528,10 +613,14 @@ mod tests { let empty_sequencers = vec![]; let genesis_evm_block_hash = [0x11u8; 32]; let program_history_root = [0x22u8; 32]; + let proof_checkpoint_root = [0x55u8; 32]; + let authorized_program_ids = authorized_program_ids(); let commit0_op_return = ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, )); let commit0 = Transaction { version: Version::TWO, @@ -565,6 +654,8 @@ mod tests { genesis_evm_block_hash, program_history_root, block_height: 1, + proof_checkpoint_root, + authorized_program_ids, }; let commit1_redeem_script = @@ -574,6 +665,8 @@ mod tests { &empty_sequencers, genesis_evm_block_hash, commit1_program_history_root, + proof_checkpoint_root, + authorized_program_ids, )); let mut commit1 = Transaction { version: Version::TWO, @@ -623,6 +716,8 @@ mod tests { genesis_evm_block_hash, program_history_root: commit1_program_history_root, block_height: 2, + proof_checkpoint_root, + authorized_program_ids, }; let commit2_redeem_script = @@ -632,6 +727,8 @@ mod tests { &empty_sequencers, genesis_evm_block_hash, commit2_program_history_root, + proof_checkpoint_root, + authorized_program_ids, )); let mut commit2 = Transaction { version: Version::TWO, @@ -689,6 +786,8 @@ mod tests { genesis_evm_block_hash, program_history_root: commit2_program_history_root, block_height: 3, + proof_checkpoint_root, + authorized_program_ids, }; let mut chain_state = CommitChainState::new(genesis_txid); @@ -709,6 +808,8 @@ mod tests { let empty_sequencers = vec![]; let genesis_evm_block_hash = [0x55u8; 32]; let program_history_root = [0x66u8; 32]; + let proof_checkpoint_root = [0x77u8; 32]; + let authorized_program_ids = authorized_program_ids(); let commit_txn = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, @@ -726,6 +827,8 @@ mod tests { &empty_sequencers, genesis_evm_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, )), }, ], @@ -742,6 +845,8 @@ mod tests { genesis_evm_block_hash, program_history_root, block_height: 1, + proof_checkpoint_root, + authorized_program_ids, }; let old_chain_commit = CircuitCommit { genesis_txid: [0x77; 32], ..commit.clone() }; diff --git a/crates/commit-chain/src/lib.rs b/crates/commit-chain/src/lib.rs index ccec01b9b..c0f207e8c 100644 --- a/crates/commit-chain/src/lib.rs +++ b/crates/commit-chain/src/lib.rs @@ -5,11 +5,11 @@ pub use commit_chain::*; pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircuitOutput { let self_program_id = input.self_program_id; - let (mut chain_state, program_history_hash) = match input.prev_proof { - CommitChainPrevProofType::GenesisBlock => ( - CommitChainState::new(input.commits[0].genesis_txid), - verifier::initial_history(verifier::ProgramType::Commit), - ), + assert!(!input.commits.is_empty(), "commit batch must be non-empty"); + let mut chain_state = match input.prev_proof { + CommitChainPrevProofType::GenesisBlock => { + CommitChainState::new(input.commits[0].genesis_txid) + } CommitChainPrevProofType::PrevProof => { println!("verify commit chain of prev proof"); let previous_program_id = verifier::verify_groth16_proof( @@ -22,18 +22,16 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui let output = decode_commit_chain_circuit_output(&input.zkm_public_values); assert_eq!(output.self_program_id, previous_program_id); - let history = verifier::next_history( - verifier::ProgramType::Commit, - output.program_history_hash, - previous_program_id, - self_program_id, + assert_eq!( + previous_program_id, self_program_id, + "commit predecessor ProgramId must match current ProgramId" ); - (output.chain_state, history) + output.chain_state } }; chain_state.apply_commit(input.commits); - CommitChainCircuitOutput { chain_state, self_program_id, program_history_hash } + CommitChainCircuitOutput { chain_state, self_program_id } } #[cfg(test)] @@ -49,7 +47,6 @@ mod circuit_output_tests { let current = bincode::serialize(&CommitChainCircuitOutput { chain_state: chain_state(), self_program_id: [1u8; 32], - program_history_hash: [2u8; 32], }) .unwrap(); assert_eq!( diff --git a/crates/header-chain/src/header_chain.rs b/crates/header-chain/src/header_chain.rs index 0583e1eb0..94fdb9ff4 100644 --- a/crates/header-chain/src/header_chain.rs +++ b/crates/header-chain/src/header_chain.rs @@ -1,7 +1,7 @@ use crate::MMRGuest; use bincode::Options as BincodeOptions; use bitcoin::{ - BlockHash, CompactTarget, TxMerkleNode, + BlockHash, CompactTarget, Network, TxMerkleNode, block::{Header, Version}, hashes::Hash, }; @@ -29,6 +29,14 @@ pub const NETWORK_TYPE: &str = { } }; +pub const NETWORK: Network = match NETWORK_TYPE.as_bytes() { + b"mainnet" => Network::Bitcoin, + b"testnet4" => Network::Testnet4, + b"signet" => Network::Signet, + b"regtest" => Network::Regtest, + _ => panic!("Unsupported network"), +}; + // Const evaluation of network type from environment const IS_REGTEST: bool = matches!(NETWORK_TYPE.as_bytes(), b"regtest"); const IS_TESTNET4: bool = matches!(NETWORK_TYPE.as_bytes(), b"testnet4"); @@ -280,6 +288,11 @@ impl ChainState { } self.total_work = current_work.to_be_bytes(); + assert_eq!( + self.block_hashes_mmr.size, + self.block_height.checked_add(1).expect("invalid empty header chain state"), + "header MMR size must equal block height plus one" + ); } } @@ -366,6 +379,7 @@ pub struct BlockHeaderCircuitOutput { pub chain_state: ChainState, pub self_program_id: verifier::ProgramId, pub program_history_hash: [u8; 32], + pub upgrade_checkpoint_hash: [u8; 32], } /// 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 25bb156e4..7c37285eb 100644 --- a/crates/header-chain/src/lib.rs +++ b/crates/header-chain/src/lib.rs @@ -18,9 +18,20 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui // println!("Detected network: {:?}", NETWORK_TYPE); // println!("NETWORK_CONSTANTS: {:?}", NETWORK_CONSTANTS); let self_program_id = input.self_program_id; - let (mut chain_state, program_history_hash) = match input.prev_proof { + let (mut chain_state, program_history_hash, upgrade_checkpoint_hash) = match input.prev_proof { HeaderChainPrevProofType::GenesisBlock => { - (ChainState::new(), verifier::initial_history(verifier::ProgramType::Header)) + let first = + input.block_headers.first().expect("genesis header batch must be non-empty"); + // Use the real Bitcoin genesis block as the first header in the header chain. + let expected = CircuitBlockHeader::from( + bitcoin::blockdata::constants::genesis_block(NETWORK).header, + ); + assert_eq!( + first, &expected, + "header chain must start at the configured Bitcoin genesis" + ); + + (ChainState::new(), verifier::initial_history(verifier::ProgramType::Header), [0u8; 32]) } HeaderChainPrevProofType::PrevProof => { println!("verify header chain of prev proof"); @@ -34,39 +45,48 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui let output = decode_header_chain_circuit_output(&input.zkm_public_values); assert_eq!(output.self_program_id, previous_program_id); + let history = verifier::next_history( verifier::ProgramType::Header, output.program_history_hash, previous_program_id, self_program_id, ); - (output.chain_state, history) + let checkpoint = if previous_program_id == self_program_id { + output.upgrade_checkpoint_hash + } else { + verifier::proof_checkpoint( + verifier::ProgramType::Header, + output.upgrade_checkpoint_hash, + previous_program_id, + self_program_id, + &input.zkm_public_values, + ) + }; + (output.chain_state, history, checkpoint) } }; chain_state.apply_blocks(input.block_headers); - BlockHeaderCircuitOutput { chain_state, self_program_id, program_history_hash } + BlockHeaderCircuitOutput { + chain_state, + self_program_id, + program_history_hash, + upgrade_checkpoint_hash, + } } #[cfg(test)] mod circuit_output_tests { use super::*; - use serde::Serialize; - - #[derive(Serialize)] - struct LegacyOutput { - chain_state: ChainState, - } #[test] - fn classifies_only_strict_current_outputs() { - let legacy = bincode::serialize(&LegacyOutput { chain_state: ChainState::new() }).unwrap(); - assert!(classify_header_chain_output(&legacy).is_err()); - + fn classifies_only_current_output() { let mut current = bincode::serialize(&BlockHeaderCircuitOutput { chain_state: ChainState::new(), self_program_id: [1u8; 32], program_history_hash: [2u8; 32], + upgrade_checkpoint_hash: [3u8; 32], }) .unwrap(); assert_eq!( diff --git a/crates/header-chain/src/mmr.rs b/crates/header-chain/src/mmr.rs index 0e8a9da2a..fb64001cc 100644 --- a/crates/header-chain/src/mmr.rs +++ b/crates/header-chain/src/mmr.rs @@ -83,61 +83,60 @@ impl MMRHost { current_index /= 2; current_level += 1; } - let (subroot_idx, internal_idx) = self.get_helpers_from_index(index); - let mmr_proof = MMRInclusionProof::new(subroot_idx, internal_idx, proof); + let mmr_proof = MMRInclusionProof::new(index, proof); (self.nodes[0][index as usize], mmr_proof) } - /// Given an index, returns the subroot index (which subtree the index is in), subtree size, and internal index (of the subtree that the index belongs to). - fn get_helpers_from_index(&self, index: u32) -> (usize, u32) { - let xor = (self.nodes[0].len() as u32) ^ index; - let xor_leading_digit = 31 - xor.leading_zeros() as usize; - let internal_idx = index & ((1 << xor_leading_digit) - 1); - let leading_zeros_size = 31 - (self.nodes[0].len() as u32).leading_zeros() as usize; - let mut subtree_idx = 0; - for i in xor_leading_digit + 1..=leading_zeros_size { - if self.nodes[0].len() & (1 << i) != 0 { - subtree_idx += 1; - } - } - (subtree_idx, internal_idx) - } - /// Verifies an inclusion proof against the current MMR root. pub fn verify_proof(&self, leaf: [u8; 32], mmr_proof: &MMRInclusionProof) -> bool { - println!("NATIVE: inclusion_proof: {mmr_proof:?}"); - println!("NATIVE: leaf: {leaf:?}"); - let sub_root = mmr_proof.get_subroot(leaf); - println!("NATIVE: calculated_sub_root: {sub_root:?}"); + let Some((subroot_idx, sub_root)) = mmr_proof.get_subroot(self.nodes[0].len() as u32, leaf) + else { + return false; + }; let sub_roots = self.get_subroots(); - println!("NATIVE: sub_roots: {sub_roots:?}"); - sub_roots[mmr_proof.subroot_idx] == sub_root + sub_roots.get(subroot_idx) == Some(&sub_root) } } #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, BorshDeserialize, BorshSerialize)] pub struct MMRInclusionProof { - pub subroot_idx: usize, - pub internal_idx: u32, + pub leaf_index: u32, pub inclusion_proof: Vec<[u8; 32]>, } impl MMRInclusionProof { - pub fn new(subroot_idx: usize, internal_idx: u32, inclusion_proof: Vec<[u8; 32]>) -> Self { - MMRInclusionProof { subroot_idx, internal_idx, inclusion_proof } + /// Creates an inclusion proof for the absolute MMR leaf index. + pub fn new(leaf_index: u32, inclusion_proof: Vec<[u8; 32]>) -> Self { + MMRInclusionProof { leaf_index, inclusion_proof } } - pub fn get_subroot(&self, leaf: [u8; 32]) -> [u8; 32] { + /// Derives the peak, internal index, and path length from the committed MMR size. + fn position(&self, mmr_size: u32) -> Option<(usize, u32, usize)> { + if mmr_size == 0 || self.leaf_index >= mmr_size { + return None; + } + let peak_level = 31 - (mmr_size ^ self.leaf_index).leading_zeros(); + let internal_mask = (1u32 << peak_level).wrapping_sub(1); + let internal_index = self.leaf_index & internal_mask; + let subroot_idx = mmr_size.checked_shr(peak_level + 1).unwrap_or(0).count_ones() as usize; + Some((subroot_idx, internal_index, peak_level as usize)) + } + + /// Reconstructs the authenticated peak for `leaf` at this proof's leaf index. + pub fn get_subroot(&self, mmr_size: u32, leaf: [u8; 32]) -> Option<(usize, [u8; 32])> { + let (subroot_idx, internal_index, expected_path_len) = self.position(mmr_size)?; + if self.inclusion_proof.len() != expected_path_len { + return None; + } let mut current_hash = leaf; - for i in 0..self.inclusion_proof.len() { - let sibling = self.inclusion_proof[i]; - if self.internal_idx & (1 << i) == 0 { + for (i, sibling) in self.inclusion_proof.iter().copied().enumerate() { + if internal_index & (1 << i) == 0 { current_hash = hash_pair(current_hash, sibling); } else { current_hash = hash_pair(sibling, current_hash); } } - current_hash + Some((subroot_idx, current_hash)) } } @@ -177,20 +176,10 @@ impl MMRGuest { /// Verifies an inclusion proof against the current MMR root pub fn verify_proof(&self, leaf: [u8; 32], mmr_proof: &MMRInclusionProof) -> bool { - //println!("GUEST: mmr_proof: {mmr_proof:?}"); - //println!("GUEST: leaf: {leaf:?}"); - let mut current_hash = leaf; - for i in 0..mmr_proof.inclusion_proof.len() { - let sibling = mmr_proof.inclusion_proof[i]; - if mmr_proof.internal_idx & (1 << i) == 0 { - current_hash = hash_pair(current_hash, sibling); - } else { - current_hash = hash_pair(sibling, current_hash); - } - } - //println!("GUEST: calculated sub_root: {current_hash:?}",); - //println!("GUEST: sub_roots: {:?}", self.subroots); - self.subroots[mmr_proof.subroot_idx] == current_hash + let Some((subroot_idx, subroot)) = mmr_proof.get_subroot(self.size, leaf) else { + return false; + }; + self.subroots.get(subroot_idx) == Some(&subroot) } } @@ -267,4 +256,22 @@ mod tests { } } } + + #[test] + fn proof_index_and_path_length_are_authenticated() { + let mut mmr = MMRHost::new(); + for i in 0..3 { + mmr.append([i; 32]); + } + let (leaf, proof) = mmr.generate_proof(0); + assert!(mmr.verify_proof(leaf, &proof)); + + let mut wrong_index = proof.clone(); + wrong_index.leaf_index = 1; + assert!(!mmr.verify_proof(leaf, &wrong_index)); + + let mut wrong_length = proof; + wrong_length.inclusion_proof.pop(); + assert!(!mmr.verify_proof(leaf, &wrong_length)); + } } diff --git a/crates/header-chain/src/spv.rs b/crates/header-chain/src/spv.rs index df91c0d21..5110c5e95 100644 --- a/crates/header-chain/src/spv.rs +++ b/crates/header-chain/src/spv.rs @@ -23,15 +23,20 @@ impl SPV { SPV { transaction, block_inclusion_proof, block_header, mmr_inclusion_proof } } - pub fn verify(&self, mmr_guest: &MMRGuest) -> bool { + /// Verifies the transaction and block inclusion proofs and returns the authenticated height. + pub fn verify(&self, mmr_guest: &MMRGuest) -> Option { let txid: [u8; 32] = self.transaction.txid(); println!("txid: {txid:?}"); let block_merkle_root = self.block_inclusion_proof.get_root(txid); println!("block_merkle_root: {block_merkle_root:?}"); println!("block_header.merkle_root: {:?}", self.block_header.merkle_root); - assert_eq!(block_merkle_root, self.block_header.merkle_root); + if block_merkle_root != self.block_header.merkle_root { + return None; + } let block_hash = self.block_header.compute_block_hash(); - mmr_guest.verify_proof(block_hash, &self.mmr_inclusion_proof) + mmr_guest + .verify_proof(block_hash, &self.mmr_inclusion_proof) + .then_some(self.mmr_inclusion_proof.leaf_index) } } @@ -181,7 +186,7 @@ mod tests { block_headers[j].clone(), mmr_proof, ); - assert!(spv.verify(&mmr_guest)); + assert_eq!(spv.verify(&mmr_guest), Some(j as u32)); } } } diff --git a/crates/state-chain/src/cbft.rs b/crates/state-chain/src/cbft.rs index 6c4a5f9d6..569235ee3 100644 --- a/crates/state-chain/src/cbft.rs +++ b/crates/state-chain/src/cbft.rs @@ -58,12 +58,11 @@ fn merkle_root_from_base64_txns(txns_b64: &[Vec]) -> [u8; 32] { pub fn verify_sequencer_commit(light_block: &LightBlock) { let vp = ProdVerifier::default(); - let verdict = vp.verify_commit(&light_block.as_untrusted_state()); - match verdict { - Verdict::Success => { - println!("success"); + let untrusted = light_block.as_untrusted_state(); + for verdict in [vp.verify_validator_sets(&untrusted), vp.verify_commit(&untrusted)] { + if !matches!(verdict, Verdict::Success) { + panic!("invalid sequencer commit: {verdict:?}"); } - v => panic!("expected success, got: {v:?}"), } } @@ -149,6 +148,14 @@ mod tests { verify_sequencer_set(light_block_1, light_block_2.clone()); } + #[test] + fn sequencer_commit_rejects_a_validator_hash_mismatch() { + let mut light_block = serde_json::from_str::(LB_1_JSON).unwrap(); + verify_sequencer_commit(&light_block); + light_block.signed_header.header.validators_hash = tendermint::Hash::Sha256([0x42; 32]); + assert!(std::panic::catch_unwind(|| verify_sequencer_commit(&light_block)).is_err()); + } + #[test] pub fn test_verify_goat_block() { // https://explorer.goat.network/block/5756298 diff --git a/crates/state-chain/src/lib.rs b/crates/state-chain/src/lib.rs index 3e802f18d..0c6929063 100644 --- a/crates/state-chain/src/lib.rs +++ b/crates/state-chain/src/lib.rs @@ -6,16 +6,19 @@ pub use state_chain::*; pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOutput { let self_program_id = input.self_program_id; - let (mut chain_state, program_history_hash) = match input.prev_proof { + let (mut chain_state, program_history_hash, upgrade_checkpoint_hash) = match input.prev_proof { StateChainPrevProofType::GenesisBlock => { + assert!(!input.blocks.is_empty(), "state chain genesis batch must be non-empty"); let block_hash: [u8; 32] = input.blocks[0].evm_block.current_block.hash_slow().into(); let block_height = input.blocks[0].evm_block.current_block.header.number; let cosmos_block = input.blocks[0].cosmos_block.clone(); ( StateChainState::new(block_height, block_hash, cosmos_block), verifier::initial_history(verifier::ProgramType::State), + [0u8; 32], ) } + StateChainPrevProofType::PrevProof => { println!("verify state chain of prev proof"); let previous_program_id = verifier::verify_groth16_proof( @@ -34,37 +37,45 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu previous_program_id, self_program_id, ); - (state_chain_output.chain_state, history) + let checkpoint = if previous_program_id == self_program_id { + state_chain_output.upgrade_checkpoint_hash + } else { + verifier::proof_checkpoint( + verifier::ProgramType::State, + state_chain_output.upgrade_checkpoint_hash, + previous_program_id, + self_program_id, + &input.zkm_public_values, + ) + }; + (state_chain_output.chain_state, history, checkpoint) } }; chain_state.apply_blocks(input.blocks); - StateChainCircuitOutput { chain_state, self_program_id, program_history_hash } + StateChainCircuitOutput { + chain_state, + self_program_id, + program_history_hash, + upgrade_checkpoint_hash, + } } #[cfg(test)] mod circuit_output_tests { use super::*; - use serde::Serialize; - - #[derive(Serialize)] - struct LegacyOutput { - chain_state: StateChainState, - } fn chain_state() -> StateChainState { StateChainState::new(1, [1u8; 32], Vec::new()) } #[test] - fn classifies_only_strict_current_outputs() { - let legacy = bincode::serialize(&LegacyOutput { chain_state: chain_state() }).unwrap(); - assert!(classify_state_chain_output(&legacy).is_err()); - + fn classifies_only_current_output() { let mut current = bincode::serialize(&StateChainCircuitOutput { chain_state: chain_state(), self_program_id: [1u8; 32], program_history_hash: [2u8; 32], + upgrade_checkpoint_hash: [3u8; 32], }) .unwrap(); assert_eq!( diff --git a/crates/state-chain/src/state_chain.rs b/crates/state-chain/src/state_chain.rs index 8e118eb68..b5ea9f66a 100644 --- a/crates/state-chain/src/state_chain.rs +++ b/crates/state-chain/src/state_chain.rs @@ -45,6 +45,7 @@ pub struct StateChainCircuitOutput { pub chain_state: StateChainState, pub self_program_id: verifier::ProgramId, pub program_history_hash: [u8; 32], + pub upgrade_checkpoint_hash: [u8; 32], } pub fn classify_state_chain_output( diff --git a/crates/store/src/localdb.rs b/crates/store/src/localdb.rs index 422dfe340..f08cc2992 100644 --- a/crates/store/src/localdb.rs +++ b/crates/store/src/localdb.rs @@ -2839,6 +2839,18 @@ impl<'a> StorageProcessor<'a> { Ok(res.rows_affected()) } + /// Deletes all persisted proof tasks for one proof chain. + pub async fn delete_long_running_task_proofs_by_name( + &mut self, + chain_name: &str, + ) -> anyhow::Result { + let res = sqlx::query("DELETE FROM long_running_task_proof WHERE chain_name = ?") + .bind(chain_name) + .execute(self.conn()) + .await?; + Ok(res.rows_affected()) + } + #[allow(clippy::too_many_arguments)] pub async fn update_long_running_task_proof_success( &mut self, diff --git a/crates/verifier/src/lib.rs b/crates/verifier/src/lib.rs index 6219b708f..8e5169595 100644 --- a/crates/verifier/src/lib.rs +++ b/crates/verifier/src/lib.rs @@ -7,7 +7,6 @@ pub type ProgramId = [u8; 32]; pub enum ProgramType { Header = 1, State = 2, - Commit = 3, } fn tagged_hash(tag: &[u8], parts: &[&[u8]]) -> [u8; 32] { @@ -22,7 +21,7 @@ fn tagged_hash(tag: &[u8], parts: &[&[u8]]) -> [u8; 32] { fn program_id_with_part_vk(zkm_vk_hash: &str, part_stark_vk: &[u8]) -> Result { let vk_hash = decode_zkm_vkey_hash(zkm_vk_hash).map_err(|e| format!("{e:?}"))?; let part_vk_hash: [u8; 32] = Sha256::digest(part_stark_vk).into(); - Ok(tagged_hash(b"bitvm2/program-id/v1", &[&vk_hash, &part_vk_hash])) + Ok(tagged_hash(b"bitvm/program-id/v1", &[&vk_hash, &part_vk_hash])) } pub fn program_id(zkm_vk_hash: &[u8], zkm_version: &str) -> Result { @@ -31,7 +30,7 @@ pub fn program_id(zkm_vk_hash: &[u8], zkm_version: &str) -> Result [u8; 32] { - tagged_hash(b"bitvm2/vk-history-seed/v1", &[&[program_type as u8]]) + tagged_hash(b"bitvm/vk-history-seed/v1", &[&[program_type as u8]]) } pub fn next_history( @@ -44,7 +43,7 @@ pub fn next_history( previous_history } else { tagged_hash( - b"bitvm2/vk-history-step/v1", + b"bitvm/vk-history-step/v1", &[&[program_type as u8], &previous_history, &previous_program_id], ) } @@ -56,17 +55,39 @@ pub fn finalize_history( current_program_id: ProgramId, ) -> [u8; 32] { tagged_hash( - b"bitvm2/vk-history-final/v1", + b"bitvm/vk-history-final/v1", &[&[program_type as u8], &history, ¤t_program_id], ) } -pub fn program_history_root( - header_history: [u8; 32], - state_history: [u8; 32], - commit_history: [u8; 32], +pub fn program_history_root(header_history: [u8; 32], state_history: [u8; 32]) -> [u8; 32] { + tagged_hash(b"bitvm/program-history/v1", &[&header_history, &state_history]) +} + +/// Extends a circuit checkpoint with the authenticated predecessor output at an upgrade. +pub fn proof_checkpoint( + program_type: ProgramType, + previous_checkpoint: [u8; 32], + previous_program_id: ProgramId, + current_program_id: ProgramId, + previous_public_values: &[u8], ) -> [u8; 32] { - tagged_hash(b"bitvm2/program-history/v1", &[&header_history, &state_history, &commit_history]) + let public_values_hash: [u8; 32] = Sha256::digest(previous_public_values).into(); + tagged_hash( + b"bitvm/proof-upgrade/v1", + &[ + &[program_type as u8], + &previous_checkpoint, + &previous_program_id, + ¤t_program_id, + &public_values_hash, + ], + ) +} + +/// Combines the Header and State upgrade checkpoints committed by the Publisher. +pub fn proof_checkpoint_root(header_checkpoint: [u8; 32], state_checkpoint: [u8; 32]) -> [u8; 32] { + tagged_hash(b"bitvm/proof-checkpoint-root/v1", &[&header_checkpoint, &state_checkpoint]) } pub fn verify_groth16_proof( @@ -90,3 +111,40 @@ pub fn verify_groth16_proof( program_id_with_part_vk(&zkm_vk_hash, part_stark_vk) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn program_history_root_binds_header_then_state() { + assert_eq!( + program_history_root([1; 32], [2; 32]), + [ + 0x50, 0x25, 0x7c, 0x01, 0xae, 0x8d, 0x1e, 0x79, 0x07, 0x4f, 0x76, 0xf6, 0x21, 0xfd, + 0x4f, 0x0c, 0x63, 0x24, 0xc0, 0x71, 0xd2, 0x79, 0x92, 0xce, 0xc7, 0xc9, 0x31, 0x4d, + 0x7c, 0xf1, 0xe3, 0xe7, + ] + ); + } + + #[test] + fn proof_checkpoint_binds_predecessor_and_upgrade_ids() { + let checkpoint = + proof_checkpoint(ProgramType::Header, [1; 32], [2; 32], [3; 32], b"public values"); + assert_ne!( + checkpoint, + proof_checkpoint(ProgramType::Header, [1; 32], [2; 32], [4; 32], b"public values",) + ); + assert_ne!( + checkpoint, + proof_checkpoint( + ProgramType::Header, + [1; 32], + [2; 32], + [3; 32], + b"other public values", + ) + ); + } +} diff --git a/node/Cargo.toml b/node/Cargo.toml index 650a031b6..74e81fc6c 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -112,8 +112,6 @@ once_cell = { workspace = true } ark-bn254 = { workspace = true } ark-groth16 = { workspace = true } -indicatif = "0.17.8" -dirs = "5.0.1" zkm-sdk = { workspace = true } zkm-verifier = { workspace = true, features = ["ark"] } diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index 3b9a5c824..14d219841 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -37,10 +37,8 @@ use bitcoin_light_client_circuit::{ estimate_tx_vbytes, }; use commit_chain::{ - CommitChainCircuitOutput, CommitChainPrevProofType, classify_commit_chain_output, -}; -use commit_chain::{ - CommitInfo, commit_chain_commitment_digest, create_sequencer_update_script, finalize, sign_raw, + AuthorizedProgramIds, CommitInfo, commit_chain_commitment_digest, + create_sequencer_update_script, finalize, sign_raw, }; use header_chain::{ HeaderChainPrevProofType, classify_header_chain_output, decode_header_chain_circuit_output, @@ -105,6 +103,81 @@ struct OutputData { fee_tx: Option, update_connector: Option, } + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ProgramIdManifest { + network: bitcoin::Network, + zkm_version: String, + authorized_program_ids: ManifestProgramIds, +} + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct ManifestProgramIds { + header: String, + state: String, + commit: String, + watchtower: String, +} + +impl ManifestProgramIds { + /// Decodes the four 0x-prefixed or plain hex ProgramIds from the release manifest. + fn decode(self) -> anyhow::Result { + Ok(AuthorizedProgramIds { + header: hex_parse::<32>(&self.header).map_err(anyhow::Error::msg)?, + state: hex_parse::<32>(&self.state).map_err(anyhow::Error::msg)?, + commit: hex_parse::<32>(&self.commit).map_err(anyhow::Error::msg)?, + watchtower: hex_parse::<32>(&self.watchtower).map_err(anyhow::Error::msg)?, + }) + } +} + +#[cfg(test)] +mod program_id_manifest_tests { + use super::*; + + #[test] + fn decodes_four_program_ids_and_rejects_removed_operator() { + let id = format!("0x{}", "01".repeat(32)); + let value = serde_json::json!({ + "header": id, + "state": id, + "commit": id, + "watchtower": id, + }); + assert_eq!( + serde_json::from_value::(value.clone()) + .unwrap() + .decode() + .unwrap() + .watchtower, + [1u8; 32] + ); + let mut invalid = value.as_object().unwrap().clone(); + invalid.insert("operator".to_string(), serde_json::Value::String(id)); + assert!( + serde_json::from_value::(serde_json::Value::Object(invalid)) + .is_err() + ); + } +} + +/// Loads and validates the ProgramIds authorized for the target Bitcoin network. +fn load_program_id_manifest( + path: &str, + expected_network: bitcoin::Network, +) -> anyhow::Result { + let manifest: ProgramIdManifest = serde_json::from_slice(&std::fs::read(path)?)?; + anyhow::ensure!( + manifest.network == expected_network, + "ProgramId manifest network does not match Bitcoin client" + ); + zkm_verifier::Groth16Verifier::get_part_stark_vk(&manifest.zkm_version); + let authorized_program_ids = manifest.authorized_program_ids.decode()?; + authorized_program_ids.validate().map_err(anyhow::Error::msg)?; + Ok(authorized_program_ids) +} impl OutputData { fn merge(&mut self, other: OutputData) { if other.fee_tx.is_some() { @@ -129,6 +202,8 @@ async fn save_commit_info( commit_info_file: &str, genesis_evm_block_hash: [u8; 32], program_history_root: [u8; 32], + proof_checkpoint_root: [u8; 32], + authorized_program_ids: AuthorizedProgramIds, ) -> Result<(), Box> { let file = std::fs::File::open(output_file)?; let output: OutputData = serde_json::from_reader(file).unwrap(); @@ -152,6 +227,8 @@ async fn save_commit_info( sequencers: sequencers.iter().cloned().map(|v| v.into()).collect(), genesis_evm_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, }; let commit_info = serde_json::to_string(&commit_info).unwrap(); @@ -230,20 +307,8 @@ enum Commands { header_chain_input_proof: String, #[arg(long)] state_chain_input_proof: String, - #[arg( - long, - required_unless_present = "commit_chain_genesis", - conflicts_with = "commit_chain_genesis" - )] - commit_chain_input_proof: Option, - #[arg(long, default_value_t = false, conflicts_with = "commit_chain_input_proof")] - commit_chain_genesis: bool, - #[arg(long, value_parser = hex_parse::<32>)] - next_header_program_id: [u8; 32], - #[arg(long, value_parser = hex_parse::<32>)] - next_state_program_id: [u8; 32], - #[arg(long, value_parser = hex_parse::<32>)] - next_commit_program_id: [u8; 32], + #[arg(long, env = "PROGRAM_ID_MANIFEST")] + program_id_manifest: String, }, Pubkey { #[arg(long, short, value_delimiter = ',')] @@ -268,6 +333,10 @@ enum Commands { goat_genesis_block_hash: [u8; 32], #[arg(long, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] program_history_root: [u8; 32], + #[arg(long, env = "PROOF_CHECKPOINT_ROOT", value_parser = hex_parse::<32>)] + proof_checkpoint_root: [u8; 32], + #[arg(long, env = "PROGRAM_ID_MANIFEST")] + program_id_manifest: String, }, PushSeq { #[arg(long, env = "OWNER_BTC_KEY_WIF")] @@ -284,6 +353,10 @@ enum Commands { goat_genesis_block_hash: [u8; 32], #[arg(long, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] program_history_root: [u8; 32], + #[arg(long, env = "PROOF_CHECKPOINT_ROOT", value_parser = hex_parse::<32>)] + proof_checkpoint_root: [u8; 32], + #[arg(long, env = "PROGRAM_ID_MANIFEST")] + program_id_manifest: String, #[arg(long)] commit_info: String, }, @@ -313,23 +386,18 @@ async fn main() -> Result<(), Box> { if let Commands::DeriveProgramHistoryRoot { header_chain_input_proof, state_chain_input_proof, - commit_chain_input_proof, - commit_chain_genesis, - next_header_program_id, - next_state_program_id, - next_commit_program_id, + program_id_manifest, } = &args.command { - let root = derive_program_history_root( + let authorized_program_ids = load_program_id_manifest(program_id_manifest, get_network())?; + let (program_history_root, proof_checkpoint_root) = derive_authorization_roots( header_chain_input_proof, state_chain_input_proof, - commit_chain_input_proof.as_deref(), - *commit_chain_genesis, - *next_header_program_id, - *next_state_program_id, - *next_commit_program_id, + authorized_program_ids.header, + authorized_program_ids.state, )?; - println!("0x{}", hex::encode(root)); + println!("program_history_root=0x{}", hex::encode(program_history_root)); + println!("proof_checkpoint_root=0x{}", hex::encode(proof_checkpoint_root)); return Ok(()); } let (btc_client, goat_client) = init_clients(&args).await?; @@ -396,7 +464,11 @@ async fn main() -> Result<(), Box> { next_publisher_btc_pubkeys, goat_genesis_block_hash, program_history_root, + proof_checkpoint_root, + program_id_manifest, } => { + let authorized_program_ids = + load_program_id_manifest(&program_id_manifest, btc_client.network())?; let (sequencer_set_hash, goat_block_number, cosmos_block_number) = get_sequencer_set_hash_from_db(&args.db_path, goat_block_number, false).await?; println!( @@ -417,6 +489,8 @@ async fn main() -> Result<(), Box> { sequencer_set_hash, goat_genesis_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, goat_block_number, ) .await @@ -429,8 +503,12 @@ async fn main() -> Result<(), Box> { init_genesis, goat_genesis_block_hash, program_history_root, + proof_checkpoint_root, + program_id_manifest, commit_info, } => { + let authorized_program_ids = + load_program_id_manifest(&program_id_manifest, btc_client.network())?; 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, init_genesis) @@ -452,6 +530,8 @@ async fn main() -> Result<(), Box> { sequencer_set_hash, goat_genesis_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, output_file, ) .await?; @@ -464,6 +544,8 @@ async fn main() -> Result<(), Box> { &commit_info, goat_genesis_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, ) .await?; Ok(()) @@ -484,101 +566,99 @@ fn load_verified_proof(path: &str) -> anyhow::Result<(Vec, verifier::Program fn next_header_history_root( path: &str, next_program_id: verifier::ProgramId, -) -> anyhow::Result<[u8; 32]> { +) -> anyhow::Result<([u8; 32], [u8; 32])> { let (public_values, previous_program_id) = load_verified_proof(path)?; - let history = match classify_header_chain_output(&public_values).map_err(anyhow::Error::msg)? { - HeaderChainPrevProofType::PrevProof => { - let output = decode_header_chain_circuit_output(&public_values); - anyhow::ensure!( - output.self_program_id == previous_program_id, - "header ProgramId mismatch" - ); - verifier::next_history( - verifier::ProgramType::Header, - output.program_history_hash, - previous_program_id, - next_program_id, - ) - } - HeaderChainPrevProofType::GenesisBlock => unreachable!(), - }; - Ok(verifier::finalize_history(verifier::ProgramType::Header, history, next_program_id)) + let (history, checkpoint) = + match classify_header_chain_output(&public_values).map_err(anyhow::Error::msg)? { + HeaderChainPrevProofType::PrevProof => { + let output = decode_header_chain_circuit_output(&public_values); + anyhow::ensure!( + output.self_program_id == previous_program_id, + "header ProgramId mismatch" + ); + ( + verifier::next_history( + verifier::ProgramType::Header, + output.program_history_hash, + previous_program_id, + next_program_id, + ), + if previous_program_id == next_program_id { + output.upgrade_checkpoint_hash + } else { + verifier::proof_checkpoint( + verifier::ProgramType::Header, + output.upgrade_checkpoint_hash, + previous_program_id, + next_program_id, + &public_values, + ) + }, + ) + } + HeaderChainPrevProofType::GenesisBlock => unreachable!(), + }; + Ok(( + verifier::finalize_history(verifier::ProgramType::Header, history, next_program_id), + checkpoint, + )) } fn next_state_history_root( path: &str, next_program_id: verifier::ProgramId, -) -> anyhow::Result<[u8; 32]> { +) -> anyhow::Result<([u8; 32], [u8; 32])> { let (public_values, previous_program_id) = load_verified_proof(path)?; - let history = match classify_state_chain_output(&public_values).map_err(anyhow::Error::msg)? { - StateChainPrevProofType::PrevProof => { - let output = decode_state_chain_circuit_output(&public_values); - anyhow::ensure!( - output.self_program_id == previous_program_id, - "state ProgramId mismatch" - ); - verifier::next_history( - verifier::ProgramType::State, - output.program_history_hash, - previous_program_id, - next_program_id, - ) - } - StateChainPrevProofType::GenesisBlock => unreachable!(), - }; - Ok(verifier::finalize_history(verifier::ProgramType::State, history, next_program_id)) -} - -fn next_commit_history_root( - path: &str, - next_program_id: verifier::ProgramId, -) -> anyhow::Result<[u8; 32]> { - let (public_values, previous_program_id) = load_verified_proof(path)?; - let history = match classify_commit_chain_output(&public_values).map_err(anyhow::Error::msg)? { - CommitChainPrevProofType::PrevProof => { - let output: CommitChainCircuitOutput = bincode::deserialize(&public_values)?; - anyhow::ensure!( - output.self_program_id == previous_program_id, - "commit ProgramId mismatch" - ); - verifier::next_history( - verifier::ProgramType::Commit, - output.program_history_hash, - previous_program_id, - next_program_id, - ) - } - CommitChainPrevProofType::GenesisBlock => unreachable!(), - }; - Ok(verifier::finalize_history(verifier::ProgramType::Commit, history, next_program_id)) + let (history, checkpoint) = + match classify_state_chain_output(&public_values).map_err(anyhow::Error::msg)? { + StateChainPrevProofType::PrevProof => { + let output = decode_state_chain_circuit_output(&public_values); + anyhow::ensure!( + output.self_program_id == previous_program_id, + "state ProgramId mismatch" + ); + ( + verifier::next_history( + verifier::ProgramType::State, + output.program_history_hash, + previous_program_id, + next_program_id, + ), + if previous_program_id == next_program_id { + output.upgrade_checkpoint_hash + } else { + verifier::proof_checkpoint( + verifier::ProgramType::State, + output.upgrade_checkpoint_hash, + previous_program_id, + next_program_id, + &public_values, + ) + }, + ) + } + StateChainPrevProofType::GenesisBlock => unreachable!(), + }; + Ok(( + verifier::finalize_history(verifier::ProgramType::State, history, next_program_id), + checkpoint, + )) } /// Verifies the predecessor proofs and derives the root expected from their next recursive steps. -fn derive_program_history_root( +fn derive_authorization_roots( header_path: &str, state_path: &str, - commit_path: Option<&str>, - commit_chain_genesis: bool, next_header_program_id: verifier::ProgramId, next_state_program_id: verifier::ProgramId, - next_commit_program_id: verifier::ProgramId, -) -> anyhow::Result<[u8; 32]> { - let commit_history_root = if commit_chain_genesis { - verifier::finalize_history( - verifier::ProgramType::Commit, - verifier::initial_history(verifier::ProgramType::Commit), - next_commit_program_id, - ) - } else { - next_commit_history_root( - commit_path.expect("commit proof is required unless genesis is selected"), - next_commit_program_id, - )? - }; - Ok(verifier::program_history_root( - next_header_history_root(header_path, next_header_program_id)?, - next_state_history_root(state_path, next_state_program_id)?, - commit_history_root, +) -> anyhow::Result<([u8; 32], [u8; 32])> { + let (header_history, header_checkpoint) = + next_header_history_root(header_path, next_header_program_id)?; + let (state_history, state_checkpoint) = + next_state_history_root(state_path, next_state_program_id)?; + Ok(( + verifier::program_history_root(header_history, state_history), + verifier::proof_checkpoint_root(header_checkpoint, state_checkpoint), )) } @@ -712,6 +792,8 @@ async fn action_push_sequencer_set_update( sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], program_history_root: [u8; 32], + proof_checkpoint_root: [u8; 32], + authorized_program_ids: AuthorizedProgramIds, output_file: &str, ) -> Result<(), Box> { let witnesses = goat_client.ss_get_sequencer_set_update_witness(goat_block_number).await?; @@ -776,6 +858,8 @@ async fn action_push_sequencer_set_update( sequencer_set_hash, goat_genesis_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, ); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, @@ -823,6 +907,8 @@ async fn action_sign_sequencer_set_update( sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], program_history_root: [u8; 32], + proof_checkpoint_root: [u8; 32], + authorized_program_ids: AuthorizedProgramIds, goat_block_number: u64, ) -> Result<(), Box> { let total = btc_public_keys.len(); @@ -843,6 +929,8 @@ async fn action_sign_sequencer_set_update( sequencer_set_hash, goat_genesis_block_hash, program_history_root, + proof_checkpoint_root, + authorized_program_ids, ); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( diff --git a/node/src/handle.rs b/node/src/handle.rs index 750c5d2d7..492f7b12a 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -12,7 +12,7 @@ use crate::soldering_payload_store::{ use crate::utils::*; use anyhow::{Context, Result, anyhow, bail}; use ark_serialize::CanonicalSerialize; -use bitcoin::{Amount, OutPoint, Txid}; +use bitcoin::{Amount, OutPoint, Txid, hashes::Hash}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{ @@ -1965,7 +1965,7 @@ async fn handle_compact_soldering_proof_operator( let prekickoff_params = build_prekickoff_params(ctx.btc_client, graph_nonce, cur_prekickoff_txn).await?; - let graph_params = build_graph_params( + let mut graph_params = build_graph_params( ctx.local_db, ctx.goat_client, instance_params, @@ -1976,7 +1976,18 @@ async fn handle_compact_soldering_proof_operator( ) .await?; + let challenge_init_txid = generate_bitvm_graph(graph_params.clone())? + .watchtower_challenge_init + .tx() + .compute_txid() + .to_byte_array(); + graph_params.pubin_disprove_constant = + get_guest_constant_value(graph_id, challenge_init_txid, &graph_params.watchtower_pubkeys)?; let mut graph = generate_bitvm_graph(graph_params)?; + anyhow::ensure!( + graph.watchtower_challenge_init.tx().compute_txid().to_byte_array() == challenge_init_txid, + "Operator constant unexpectedly changes the watchtower challenge init transaction" + ); operator_pre_sign(operator_master_key.master_keypair(), &mut graph)?; let graph = graph.to_simplified()?; diff --git a/node/src/utils.rs b/node/src/utils.rs index abb5a8dea..6deaa1328 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -64,7 +64,6 @@ use secp256k1::{Message as SecpMessage, SECP256K1, Secp256k1}; use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use std::net::SocketAddr; -use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::{Path, PathBuf}; use std::str::FromStr; use std::time::{Instant, SystemTime, UNIX_EPOCH}; @@ -105,7 +104,7 @@ 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, + IMM_GROTH16_VK_BYTES, convert_ark_imm_wrap_vk, decode_zkm_vkey_hash, load_ark_public_inputs_from_bytes, }; @@ -180,13 +179,10 @@ 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( + /// Checks graph Watchtower membership, uniqueness, and supported count. + pub(super) fn validate_watchtower_registry_selection( selected: &[XOnlyPublicKey], registered: &[XOnlyPublicKey], - graph_id: [u8; 16], - genesis_txid: [u8; 32], - constant: [u8; 32], ) -> Result<()> { use std::collections::HashSet; @@ -215,9 +211,22 @@ pub mod todo_funcs { bail!(SpecialError::InvalidGraph(format!("watchtower {} is not registered", key))); } } + Ok(()) + } + /// Validates the graph's ordered watchtower selection and its bound Operator constant. + pub(super) fn validate_watchtower_selection( + selected: &[XOnlyPublicKey], + registered: &[XOnlyPublicKey], + graph_id: [u8; 16], + genesis_txid: [u8; 32], + challenge_init_txid: [u8; 32], + constant: [u8; 32], + ) -> Result<()> { + validate_watchtower_registry_selection(selected, registered)?; let key_bytes = selected.iter().map(XOnlyPublicKey::serialize).collect::>(); - let expected = hash_operator_constant(graph_id, genesis_txid, &key_bytes); + let expected = + hash_operator_constant(graph_id, genesis_txid, challenge_init_txid, &key_bytes); if constant != expected { bail!(SpecialError::InvalidGraph( "operator constant mismatch for graph watchtower list".to_string() @@ -450,6 +459,7 @@ pub mod todo_funcs { &watchtowers_on_chain, *graph.parameters.graph_id.as_bytes(), get_genesis_sequencer_commit_id(), + full_graph.watchtower_challenge_init.tx().compute_txid().to_byte_array(), graph.parameters.pubin_disprove_constant, )?; @@ -587,6 +597,7 @@ pub mod todo_funcs { &watchtowers_on_chain, *graph.parameters.graph_id.as_bytes(), get_genesis_sequencer_commit_id(), + full_graph.watchtower_challenge_init.tx().compute_txid().to_byte_array(), graph.parameters.pubin_disprove_constant, )?; @@ -2583,8 +2594,7 @@ pub fn build_operator_guest_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}")) + Ok(Vec::from(zkm_verifier::Groth16Verifier::get_part_stark_vk(zkm_version))) } fn combined_operator_vk_hash(operator_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { @@ -2624,14 +2634,20 @@ pub fn derive_operator_static_input() -> Result { Ok(operator_identity()?.2) } +/// Derives the Operator proof identity and graph constant for one challenge-init transaction. pub fn derive_operator_statement( graph_id: Uuid, + challenge_init_txid: [u8; 32], 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); + let constant = hash_operator_constant( + *graph_id.as_bytes(), + get_genesis_sequencer_commit_id(), + challenge_init_txid, + &key_bytes, + ); Ok(OperatorStatement { static_input, vk_hash, zkm_version, constant }) } @@ -2726,8 +2742,11 @@ pub async fn get_operator_proof( return Ok((None, get_operator_proof_wait_secs())); }; - let statement = - derive_operator_statement(graph_id, &bitvm_graph.parameters.watchtower_pubkeys)?; + let statement = derive_operator_statement( + graph_id, + bitvm_graph.watchtower_challenge_init.tx().compute_txid().to_byte_array(), + &bitvm_graph.parameters.watchtower_pubkeys, + )?; if statement.constant != bitvm_graph.parameters.pubin_disprove_constant { bail!("graph operator constant does not match its watchtower list"); } @@ -3402,7 +3421,6 @@ pub async fn build_graph_params( graph_nonce: u64, 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(); @@ -3422,17 +3440,7 @@ pub async fn build_graph_params( .to_byte_array() }) .collect(); - 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, - )?; + todo_funcs::validate_watchtower_registry_selection(&watchtower_pubkeys, &watchtower_pubkeys)?; Ok(BitvmGcGraphParameters { instance_parameters, prekickoff_parameters, @@ -3446,7 +3454,7 @@ pub async fn build_graph_params( operator_receive_address, watchtower_pubkeys, watchtower_ack_hashlocks, - pubin_disprove_constant, + pubin_disprove_constant: [0u8; 32], gc_data: bitvm_gc_circuit_datas, }) } @@ -5747,13 +5755,19 @@ pub(super) async fn find_instances_by_escrow_hash<'a>( if size > 0 { Ok(Some(instances[0].clone())) } else { Ok(None) } } +/// Computes the graph constant committed by the Operator guest and Connector D. pub fn get_guest_constant_value( - _instance_id: Uuid, graph_id: Uuid, + challenge_init_txid: [u8; 32], 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)) + Ok(hash_operator_constant( + graph_id.into_bytes(), + get_genesis_sequencer_commit_id(), + challenge_init_txid, + &key_bytes, + )) } pub(crate) async fn get_bridge_out_global_stats<'a>( storage_processor: &mut StorageProcessor<'a>, diff --git a/node/src/vk.rs b/node/src/vk.rs index bac061240..63867ab93 100644 --- a/node/src/vk.rs +++ b/node/src/vk.rs @@ -1,123 +1,10 @@ -#![allow(dead_code)] - -use anyhow::{Result, ensure}; -use std::fs; -use std::path::PathBuf; -use zkm_sdk::install::CIRCUIT_ARTIFACTS_URL_BASE; +use anyhow::Result; use zkm_verifier::{IMM_GROTH16_VK_BYTES, load_ark_groth16_verifying_key_from_bytes}; -use { - futures::StreamExt, - indicatif::{ProgressBar, ProgressStyle}, - 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; - - // Handle case if we're already in a tokio runtime. - if let Ok(handle) = tokio::runtime::Handle::try_current() { - block_in_place(|| handle.block_on(fut)) - } else { - // Otherwise create a new runtime. - let rt = tokio::runtime::Runtime::new().expect("Failed to create a new runtime"); - rt.block_on(fut) - } -} - pub async fn get_vk() -> Result { - let build_dir = try_install_circuit_artifacts(); - let vk_file = build_dir.join("groth16_vk.bin"); - let content = fs::read(&vk_file)?; - ensure!( - content.as_slice() == *IMM_GROTH16_VK_BYTES, - "Groth16 verifying key at {} does not match embedded IMM Groth16 verifying key", - vk_file.display() - ); - - Ok(load_ark_groth16_verifying_key_from_bytes(&content)?) -} - -#[must_use] -pub fn groth16_circuit_artifacts_dir() -> PathBuf { - dirs::home_dir().unwrap().join(".zkm").join("circuits/groth16/imm-wrap-vk") -} - -/// Tries to install the groth16 circuit artifacts if they are not already installed. -#[must_use] -pub fn try_install_circuit_artifacts() -> PathBuf { - let artifacts_type = "groth16"; - let build_dir = groth16_circuit_artifacts_dir(); - - if build_dir.exists() { - println!( - "[ziren] {} circuit artifacts already seem to exist at {}. if you want to re-download them, delete the directory", - artifacts_type, - build_dir.display() - ); - } else { - install_circuit_artifacts(build_dir.clone(), artifacts_type); - } - build_dir -} - -#[allow(clippy::needless_pass_by_value)] -pub fn install_circuit_artifacts(build_dir: PathBuf, artifacts_type: &str) { - // Create the build directory. - fs::create_dir_all(&build_dir).expect("failed to create build directory"); - - // Download the artifacts. - let download_url = format!("{CIRCUIT_ARTIFACTS_URL_BASE}/{artifacts_type}-imm-wrap-vk.tar.gz"); - let mut artifacts_tar_gz_file = - tempfile::NamedTempFile::new().expect("failed to create tempfile"); - let client = reqwest::Client::builder().build().expect("failed to create reqwest client"); - block_on(download_file(&client, &download_url, &mut artifacts_tar_gz_file)) - .expect("failed to download file"); - - // Extract the tarball to the build directory. - let mut res = Command::new("tar") - .args([ - "-Pxzf", - artifacts_tar_gz_file.path().to_str().unwrap(), - "-C", - build_dir.to_str().unwrap(), - ]) - .spawn() - .expect("failed to extract tarball"); - res.wait().unwrap(); - - println!("[zkm] downloaded {} to {:?}", download_url, build_dir.to_str().unwrap(),); -} - -pub async fn download_file( - client: &reqwest::Client, - url: &str, - file: &mut impl std::io::Write, -) -> std::result::Result<(), String> { - let res = client.get(url).send().await.or(Err(format!("Failed to GET from '{}'", &url)))?; - - let total_size = - res.content_length().ok_or(format!("Failed to get content length from '{}'", &url))?; - - let pb = ProgressBar::new(total_size); - pb.set_style(ProgressStyle::default_bar() - .template("{spinner:.green} [{elapsed_precise}] [{wide_bar:.cyan/blue}] {bytes}/{total_bytes} ({bytes_per_sec}, {eta})").unwrap() - .progress_chars("#>-")); - - let mut downloaded: u64 = 0; - let mut stream = res.bytes_stream(); - while let Some(item) = stream.next().await { - let chunk = item.or(Err("Error while downloading file"))?; - file.write_all(&chunk).or(Err("Error while writing to file"))?; - let new = min(downloaded + (chunk.len() as u64), total_size); - downloaded = new; - pb.set_position(new); - } - pb.finish(); - - Ok(()) + Ok(load_ark_groth16_verifying_key_from_bytes(&IMM_GROTH16_VK_BYTES)?) } #[cfg(test)] @@ -125,7 +12,6 @@ mod tests { use super::*; #[tokio::test] - #[ignore] async fn test_get_vk() { let vk = get_vk().await.unwrap(); let imm_v = diff --git a/proof-builder-rpc/src/task/commit_chain_proof.rs b/proof-builder-rpc/src/task/commit_chain_proof.rs index 5cb4be1a1..93e1cd24f 100644 --- a/proof-builder-rpc/src/task/commit_chain_proof.rs +++ b/proof-builder-rpc/src/task/commit_chain_proof.rs @@ -4,6 +4,7 @@ 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 commit_chain_proof::load_upgrade_commits; use proof_builder::{ProofBuilder, ProofRequest}; use std::time::Duration; use store::localdb::LocalDB; @@ -32,43 +33,55 @@ pub(crate) fn spawn_commit_chain_proof_task( loop { tokio::select! { _ = tokio::time::sleep(Duration::from_secs(interval)) => { - 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 = fetch_next_commit_task_index(&local_db).await?; - args.input_proof = next_task.path_to_proof.unwrap(); - args.commit_info = format!( - "{}/commit_info.json.{}", - std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), - args.start, - ); - args.output_proof = format!( - "{}/{}-{}.bin", - std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), - args.start, - args.batch_size - ); - args.commits = format!( - "{}/{}-{}.bin.commits", - std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), - args.start, - args.batch_size - ); - args.init_input = false; + let starts_from_genesis = args.starts_from_genesis(); + if !starts_from_genesis { + 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 = fetch_next_commit_task_index(&local_db).await?; + args.input_proof = next_task.path_to_proof.unwrap(); + args.commit_info = format!( + "{}/commit_info.json.{}", + std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), + args.start, + ); + args.output_proof = format!( + "{}/{}-{}.bin", + std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), + args.start, + args.batch_size + ); + args.commits = format!( + "{}/{}-{}.bin.commits", + std::path::Path::new(&args.output_proof).parent().unwrap().to_str().unwrap(), + args.start, + args.batch_size + ); + args.init_input = false; + } } 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.bitcoin_network).await { + let mut commits = match fetch_commit_chain(&args.esplora_url, &args.commit_info, &args.commits, args.bitcoin_network).await { Ok(d) => d, Err(err) => { tracing::warn!("Fetch commit chain error, {err:?}, continuing"); continue; } }; + if let Some(path) = args.upgrade_commits.as_deref() { + commits = match load_upgrade_commits(path, &commits[0]) { + Ok(commits) => commits, + Err(err) => { + tracing::warn!("Load upgrade commits error, {err:?}, continuing"); + continue; + } + }; + } let block_start = commits.first().unwrap().block_height as i64; let ctx = ProofRequest::CommitChainProofRequest { - init_input: args.init_input, + init_input: starts_from_genesis, input_proof: args.input_proof.clone(), output_proof: args.output_proof.clone(), commit_info: args.commit_info.clone(), @@ -86,7 +99,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_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(), starts_from_genesis, 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 4cc88c0b5..75efccbe3 100644 --- a/proof-builder-rpc/src/task/mod.rs +++ b/proof-builder-rpc/src/task/mod.rs @@ -551,7 +551,7 @@ 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. +/// Persists a Commit proof, replacing prior Commit records for a Genesis replay. pub(crate) async fn create_commit_chain_proof( local_db: &LocalDB, start: i64, @@ -561,28 +561,35 @@ pub(crate) async fn create_commit_chain_proof( proof_size: i64, cycles: u64, chain_name: String, + replace_existing: bool, 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, chain_name.clone()) - .await?; - tracing::info!("previous_proof: {previous_proof:?}"); - if let Some(previous_proof) = previous_proof { - let prev_batch_size = start - previous_proof.block_start; - tracing::info!("update previous proof from {start} batch_size: {prev_batch_size}"); + if replace_existing { storage_processor - .update_long_running_task_proof_state( - previous_proof.block_start, - &previous_proof.chain_name, - prev_batch_size, - previous_proof.proof_state, - ) + .delete_long_running_task_proofs_by_name(&CommitChainProofBuilder::name()) + .await?; + } else { + // We use start directly since block_end is initialized by u64::MAX. + let previous_proof = storage_processor + .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 - 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, + &previous_proof.chain_name, + prev_batch_size, + previous_proof.proof_state, + ) + .await?; + } } let affected = storage_processor .create_long_running_task_proof(&LongRunningTaskProof { @@ -935,6 +942,114 @@ mod tests { use store::create_local_db; use uuid::Uuid; + fn long_running_task(chain_name: &str, block_start: i64, path: &str) -> LongRunningTaskProof { + LongRunningTaskProof { + block_start, + block_end: block_start + 10, + chain_name: chain_name.to_string(), + path_to_proof: Some(path.to_string()), + proof_state: ProofState::Proven.to_i64(), + ..LongRunningTaskProof::default() + } + } + + #[tokio::test] + async fn genesis_commit_proof_replaces_only_commit_chain_records() { + let local_db = create_local_db("sqlite::memory:").await; + let mut storage = local_db.acquire().await.unwrap(); + storage + .create_long_running_task_proof(&long_running_task("commit-chain", 10, "old-1")) + .await + .unwrap(); + storage + .create_long_running_task_proof(&long_running_task("commit-chain", 20, "old-2")) + .await + .unwrap(); + storage + .create_long_running_task_proof(&long_running_task("header-chain", 0, "header")) + .await + .unwrap(); + let header_count = storage + .find_all_running_task_proofs_by_name(HeaderChainProofBuilder::name()) + .await + .unwrap() + .len(); + drop(storage); + + create_commit_chain_proof( + &local_db, + 10, + 100, + "new".to_string(), + "public-values".to_string(), + 1, + 2, + CommitChainProofBuilder::name(), + true, + 3, + 4, + ProofState::Proven, + "v1.2.5".to_string(), + ) + .await + .unwrap(); + + let mut storage = local_db.acquire().await.unwrap(); + let commits = storage + .find_all_running_task_proofs_by_name(CommitChainProofBuilder::name()) + .await + .unwrap(); + assert_eq!(commits.len(), 1); + assert_eq!(commits[0].path_to_proof.as_deref(), Some("new")); + assert_eq!( + storage + .find_all_running_task_proofs_by_name(HeaderChainProofBuilder::name()) + .await + .unwrap() + .len(), + header_count + ); + } + + #[tokio::test] + async fn failed_genesis_commit_insert_rolls_back_old_record_deletion() { + let local_db = create_local_db("sqlite::memory:").await; + let mut storage = local_db.acquire().await.unwrap(); + storage + .create_long_running_task_proof(&long_running_task("commit-chain", 10, "old")) + .await + .unwrap(); + drop(storage); + + assert!( + create_commit_chain_proof( + &local_db, + 10, + 100, + "new".to_string(), + "public-values".to_string(), + 1, + 2, + "invalid-chain".to_string(), + true, + 3, + 4, + ProofState::Proven, + "v1.2.5".to_string(), + ) + .await + .is_err() + ); + + let mut storage = local_db.acquire().await.unwrap(); + let commits = storage + .find_all_running_task_proofs_by_name(CommitChainProofBuilder::name()) + .await + .unwrap(); + assert_eq!(commits.len(), 1); + assert_eq!(commits[0].path_to_proof.as_deref(), Some("old")); + } + #[tokio::test] async fn test_add_watchtower_task() { let db_path = std::env::var("TEST_DB") diff --git a/proof-builder-rpc/src/task/operator_proof.rs b/proof-builder-rpc/src/task/operator_proof.rs index 3b9f87e40..a35243cb1 100644 --- a/proof-builder-rpc/src/task/operator_proof.rs +++ b/proof-builder-rpc/src/task/operator_proof.rs @@ -81,12 +81,10 @@ 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, - watchtower_challenge_txn_scripts, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, ) = match fetch_target_block_and_watchtower_tx( &args.esplora_url, &args.latest_sequencer_commit_txid, @@ -122,12 +120,10 @@ 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, - watchtower_challenge_txn_scripts, + watchtower_challenge_init_txid, + watchtower_challenge_init_txn, + watchtower_challenge_witnesses, }; let proving_start = tokio::time::Instant::now(); let (cycles, proving_time, public_value_hex, proof_size, proof_state, zkm_version) = match builder.build_proof(&ctx) {