From bb6f27e4048380d126f5a2b944c1e293f8d8be31 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/5] 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 df4b113aa..cf80dfb6b 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 400281e82..57b6ce796 100644 --- a/circuits/commit-chain-proof/host/src/lib.rs +++ b/circuits/commit-chain-proof/host/src/lib.rs @@ -205,9 +205,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(), @@ -263,10 +262,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 9710a5d6b..82141fbe3 100644 --- a/circuits/header-chain-proof/host/src/lib.rs +++ b/circuits/header-chain-proof/host/src/lib.rs @@ -219,9 +219,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(), @@ -282,9 +281,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 e908c95aa..9c48a8695 100644 --- a/circuits/state-chain-proof/host/src/lib.rs +++ b/circuits/state-chain-proof/host/src/lib.rs @@ -335,10 +335,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(), @@ -389,9 +387,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 bb51246e2..30428d51e 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 9bfd2a681..ab7667b18 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 3303d73c5d27dd86d7c21e97a25e7f4cc42a04e0 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/5] 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 d7aa2cccf..f82abfe3a 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -121,7 +121,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 0157a8ff5c8648b9b3543faea5939f18aea7cb9c 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/5] 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 376fc300dad256a38bacbde673614ca3fa34324d 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/5] 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 cf80dfb6b..a9f203e57 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", @@ -13923,6 +13931,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] @@ -14142,6 +14151,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 57b6ce796..6f32a5a8d 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, @@ -149,6 +153,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 { @@ -188,6 +197,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) => { @@ -205,20 +215,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(), ), }; @@ -227,6 +233,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 82141fbe3..91bfe4f0f 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, @@ -156,6 +166,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 { @@ -203,6 +218,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) => { @@ -219,20 +235,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(), ), }; @@ -250,6 +262,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 9c006875b..532a52aed 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -326,12 +326,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![], } }; @@ -353,12 +356,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![], } }; @@ -380,6 +386,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 @@ -387,6 +395,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 9c48a8695..2b2e6e141 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, @@ -281,6 +285,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 { @@ -318,6 +327,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) => { @@ -335,20 +345,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(), ), }; @@ -358,6 +364,7 @@ impl ProofBuilder for StateChainProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: blocks.clone(), }; // Generate the proofs. @@ -391,6 +398,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 f82abfe3a..e66624c81 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)] @@ -93,6 +97,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 { @@ -148,6 +157,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 @@ -155,6 +166,7 @@ impl ProofBuilder for WatchtowerProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers: vec![], } }; @@ -176,12 +188,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 30428d51e..1f6c2b64f 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 ab7667b18..94558ee78 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 f805d25a1..38b57257c 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -128,4 +128,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 480b43a9b..2ac2ea35a 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -1656,15 +1656,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 9745b861c7793d23c1b500e87e067cb64724a052 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/5] 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 6f32a5a8d..cf26f67d1 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 2b2e6e141..9e098f527 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, } @@ -398,11 +411,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 e66624c81..558cd199f 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, }