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 1/9] 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 2/9] 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 3/9] 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 4/9] 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 5/9] 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 6/9] 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 7/9] 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 8/9] 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 9/9] 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],