diff --git a/Cargo.lock b/Cargo.lock index fb7561fb9..976e36fa4 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", @@ -6029,6 +6034,7 @@ dependencies = [ "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -6057,6 +6063,7 @@ dependencies = [ "tracing", "tracing-subscriber 0.3.23", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -8641,6 +8648,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "url", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -9745,6 +9753,7 @@ dependencies = [ "state-chain", "strum 0.26.3", "thiserror 1.0.69", + "verifier", "zkm-sdk", ] @@ -12476,6 +12485,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -12513,6 +12523,7 @@ dependencies = [ "tracing-subscriber 0.3.23", "url", "util", + "verifier", "zkm-build", "zkm-prover", "zkm-sdk", @@ -13934,6 +13945,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] @@ -14153,6 +14165,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 e5fce4076..0706bbbeb 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, @@ -32,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")] @@ -100,11 +111,18 @@ pub async fn fetch_commit_chain( }; let commit_txn = btc_client.get_tx(&txid).await?.unwrap(); - let op_return_data = extract_op_return_data(&commit_txn.output); - let commitment = parse_commit_chain_commitment(&op_return_data); - - if let tendermint::Hash::Sha256(expected_hash) = sequencer_hash(&ci.sequencers) { - assert_eq!(expected_hash, commitment.sequencer_set_hash); + let commitment = + extract_commit_chain_commitment(&commit_txn.output).map_err(anyhow::Error::msg)?; + if let tendermint::Hash::Sha256(sequencer_set_hash) = sequencer_hash(&ci.sequencers) { + anyhow::ensure!( + commitment + == commit_chain_commitment_digest( + sequencer_set_hash, + ci.genesis_evm_block_hash, + ci.program_history_root, + ), + "commit transaction digest does not match commit info" + ); } else { panic!("Invalid sequencer set hash"); } @@ -128,6 +146,8 @@ pub async fn fetch_commit_chain( next_publisher_public_keys, next_threshold: ci.next_threshold, genesis_txid: Txid::from_str(&ci.genesis_txid)?.as_raw_hash().to_byte_array(), + genesis_evm_block_hash: ci.genesis_evm_block_hash, + program_history_root: ci.program_history_root, block_height, }; commits.push(commit); @@ -189,6 +209,7 @@ impl ProofBuilder for CommitChainProofBuilder { //let prev: CommitChainCircuitOutput = serde_json::from_slice(&public_inputs).unwrap(); Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { @@ -206,21 +227,16 @@ 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), - 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(), ), }; @@ -229,6 +245,7 @@ impl ProofBuilder for CommitChainProofBuilder { zkm_version, zkm_proof, prev_proof, + self_program_id, commits: commits.to_vec(), zkm_public_values, }; @@ -264,10 +281,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/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 cd02d2928..4aee28cd5 100644 --- a/circuits/header-chain-proof/guest/Cargo.lock +++ b/circuits/header-chain-proof/guest/Cargo.lock @@ -1111,12 +1111,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -2773,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 e107db823..7801b90eb 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, @@ -206,6 +216,7 @@ impl ProofBuilder for HeaderChainProofBuilder { Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { @@ -222,21 +233,16 @@ 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), - 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(), ), }; @@ -254,6 +260,7 @@ impl ProofBuilder for HeaderChainProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers, }; @@ -285,9 +292,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/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 91821b9a1..5ebbd2d98 100644 --- a/circuits/operator-proof/guest/Cargo.lock +++ b/circuits/operator-proof/guest/Cargo.lock @@ -2428,12 +2428,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5186,6 +5188,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5758,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..f95d59ba4 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] = [ + 0x38, 0x72, 0x34, 0xd6, 0x8c, 0x91, 0xae, 0x7f, 0xaa, 0x8e, 0xa7, 0x20, 0x70, 0x95, 0xdc, 0x36, + 0x4c, 0x58, 0xef, 0xd0, 0x47, 0x71, 0x5d, 0x62, 0x49, 0x86, 0x61, 0x88, 0xa8, 0x10, 0x10, 0x3b, +]; + pub fn main() { // calculate operator public input: https://github.com/ProjectZKM/Ziren/blob/main/crates/sdk/src/utils.rs#L42 let included_watchtowers: U256 = zkm_zkvm::io::read::(); @@ -42,6 +48,7 @@ pub fn main() { watchtower_challenge_txn_scripts, watchtower_challenge_txn_prev_outs, &graph_watchtower_xonly_public_keys, + EXPECTED_WATCHTOWER_PROGRAM_ID, operator_header_chain, operator_commit_chain, operator_state_chain, diff --git a/circuits/operator-proof/host/Cargo.toml b/circuits/operator-proof/host/Cargo.toml index 186054947..7b22071a1 100644 --- a/circuits/operator-proof/host/Cargo.toml +++ b/circuits/operator-proof/host/Cargo.toml @@ -43,6 +43,7 @@ cbft-rpc = { workspace = true } bitcoin = { workspace = true } client = { workspace = true } proof-builder.workspace = true +verifier.workspace = true # Ziren zkm-sdk.workspace = true diff --git a/circuits/operator-proof/host/src/lib.rs b/circuits/operator-proof/host/src/lib.rs index 5c62a4ae8..be3ddfffd 100644 --- a/circuits/operator-proof/host/src/lib.rs +++ b/circuits/operator-proof/host/src/lib.rs @@ -25,6 +25,10 @@ use zkm_sdk::{ /// The arguments for the cli. #[derive(Debug, Clone, Parser, serde::Deserialize, serde::Serialize)] pub struct Args { + #[arg(long, default_value_t = false)] + #[serde(default)] + pub print_program_id: bool, + #[arg(long, default_value_t = true)] pub enable: bool, @@ -34,40 +38,119 @@ pub struct Args { #[arg(long, env, default_value_t = Network::Regtest)] pub bitcoin_network: Network, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub included_watchtowers: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub graph_id: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub latest_sequencer_commit_txid: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub operator_committed_blockhash: String, - #[clap(long, env)] + #[clap( + long, + env, + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub genesis_sequencer_commit_txid: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'H', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub header_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'c', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub commit_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 's', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub state_chain_input_proof: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'e', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("0")) + )] pub execution_layer_block_number: u64, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 't', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_challenge_txids: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'w', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_public_keys: String, - #[clap(long, env, short)] + #[clap( + long, + env, + short = 'i', + required = false, + required_unless_present = "print_program_id", + default_value_if("print_program_id", "true", Some("")) + )] pub watchtower_challenge_init_txid: String, #[clap(long, env, default_value = "commit-proof.bin")] @@ -334,12 +417,15 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; HeaderChainCircuitInput { prev_proof: HeaderChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers: vec![], } }; @@ -361,12 +447,15 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; CommitChainCircuitInput { prev_proof: CommitChainPrevProofType::GenesisBlock, // unused zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, commits: vec![], } }; @@ -387,6 +476,8 @@ impl ProofBuilder for OperatorProofBuilder { format!("invalid UTF-8 in zkm_version file '{version_path}'") }) })?; + let self_program_id = + verifier::program_id(&zkm_vk_hash, &zkm_version).map_err(anyhow::Error::msg)?; StateChainCircuitInput { prev_proof: StateChainPrevProofType::GenesisBlock, // unused @@ -394,6 +485,7 @@ impl ProofBuilder for OperatorProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: vec![], } }; diff --git a/circuits/operator-proof/host/src/main.rs b/circuits/operator-proof/host/src/main.rs index 9101b09e8..95a2ddc35 100644 --- a/circuits/operator-proof/host/src/main.rs +++ b/circuits/operator-proof/host/src/main.rs @@ -3,6 +3,7 @@ use clap::Parser; use operator_proof::{Args, OperatorProofBuilder, fetch_target_block_and_watchtower_tx}; use proof_builder::{ProofBuilder, ProofRequest}; use util::hex_parse; +use zkm_sdk::HashableKey; #[tokio::main] async fn main() { @@ -11,6 +12,13 @@ async fn main() { // Setup the logger. zkm_sdk::utils::setup_logger(); + let builder = OperatorProofBuilder::new(); + if args.print_program_id { + println!("OPERATOR_PROGRAM_ID={}", hex::encode(builder.program_id().unwrap())); + eprintln!("OPERATOR_VK_HASH={}", builder.vk().bytes32()); + return; + } + let ( block_pos_ss_commit, target_block_ss_commit, @@ -34,8 +42,6 @@ async fn main() { .await .unwrap(); - let builder = OperatorProofBuilder::new(); - let ctx = ProofRequest::OperatorProofRequest { included_watchtowers: args.included_watchtowers.clone(), graph_id: hex_parse::<16>(&args.graph_id).unwrap(), diff --git a/circuits/proof-builder/Cargo.toml b/circuits/proof-builder/Cargo.toml index cedd64512..f73139360 100644 --- a/circuits/proof-builder/Cargo.toml +++ b/circuits/proof-builder/Cargo.toml @@ -13,6 +13,7 @@ serde.workspace = true zkm-sdk.workspace = true #zkm-prover.workspace = true #zkm-verifier.workspace = true +verifier.workspace = true strum = { workspace = true, features = ["derive"] } # diff --git a/circuits/proof-builder/src/lib.rs b/circuits/proof-builder/src/lib.rs index d90bc5ae7..528fce114 100644 --- a/circuits/proof-builder/src/lib.rs +++ b/circuits/proof-builder/src/lib.rs @@ -7,7 +7,7 @@ use state_chain::CircuitStateBlock; use std::fs; use strum::{Display, EnumString}; use thiserror::Error; -use zkm_sdk::{ProverClient, ZKMProofWithPublicValues}; +use zkm_sdk::{HashableKey, ProverClient, ZKM_CIRCUIT_VERSION, ZKMProofWithPublicValues}; use zkm_sdk::{ZKMProvingKey, ZKMVerifyingKey}; #[derive(Debug, Clone, Serialize, Deserialize)] @@ -87,6 +87,12 @@ pub trait ProofBuilder { fn pk(&self) -> &ZKMProvingKey; fn vk(&self) -> &ZKMVerifyingKey; + /// Returns the Program ID derived from the builder's verifying key. + fn program_id(&self) -> Result { + verifier::program_id(self.vk().bytes32().as_bytes(), ZKM_CIRCUIT_VERSION) + .map_err(anyhow::Error::msg) + } + fn build_proof( &self, ctx: &ProofRequest, diff --git a/circuits/state-chain-proof/guest/Cargo.lock b/circuits/state-chain-proof/guest/Cargo.lock index 226658301..7084b519b 100644 --- a/circuits/state-chain-proof/guest/Cargo.lock +++ b/circuits/state-chain-proof/guest/Cargo.lock @@ -2377,12 +2377,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5135,6 +5137,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5707,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 b29a6a360..40f4593c1 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, @@ -66,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, } @@ -319,6 +336,7 @@ impl ProofBuilder for StateChainProofBuilder { Some(public_inputs) }; + let self_program_id = self.program_id()?; let (prev_proof, zkm_proof, zkm_public_values, zkm_vk_hash, zkm_version) = match prev_receipt.clone() { Some(public_inputs) => { @@ -336,22 +354,16 @@ 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), - 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(), ), }; @@ -361,6 +373,7 @@ impl ProofBuilder for StateChainProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, blocks: blocks.clone(), }; // Generate the proofs. @@ -390,9 +403,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/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 0dd35c975..573afc9fe 100644 --- a/circuits/watchtower-proof/guest/Cargo.lock +++ b/circuits/watchtower-proof/guest/Cargo.lock @@ -2428,12 +2428,14 @@ checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" name = "header-chain" version = "0.4.0" dependencies = [ + "bincode", "bitcoin", "borsh", "crypto-bigint", "serde", "sha2 0.10.9", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5186,6 +5188,7 @@ dependencies = [ "tendermint-light-client-verifier", "tracing", "verifier", + "zkm-primitives", "zkm-zkvm", ] @@ -5758,6 +5761,7 @@ dependencies = [ name = "verifier" version = "0.4.0" dependencies = [ + "sha2 0.10.9", "zkm-verifier", ] 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/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 e0023bc0e..a52da06da 100644 --- a/circuits/watchtower-proof/host/src/lib.rs +++ b/circuits/watchtower-proof/host/src/lib.rs @@ -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, @@ -34,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)] + #[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, } @@ -122,7 +166,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, @@ -150,6 +193,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 @@ -157,6 +202,7 @@ impl ProofBuilder for WatchtowerProofBuilder { zkm_public_values, zkm_vk_hash, zkm_version, + self_program_id, block_headers: vec![], } }; @@ -178,12 +224,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![], } }; @@ -204,18 +253,20 @@ 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![], } }; // --- 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 +298,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/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 9cb38b9f7..dfa5703a1 100644 --- a/crates/bitcoin-light-client-circuit/src/lib.rs +++ b/crates/bitcoin-light-client-circuit/src/lib.rs @@ -2,31 +2,44 @@ 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, - extract_data_from_commitment_outputs, parse_commit_chain_commitment, sequencer_hash, + CommitChainCircuitInput, commit_chain_commitment_digest, decode_commit_chain_circuit_output, + extract_commit_chain_commitment, extract_data_from_commitment_outputs, sequencer_hash, }; use header_chain::{ - BitcoinMerkleTree, 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::{ - ScriptBuf, Transaction, TxOut, Txid, + ScriptBuf, Transaction, TxOut, secp256k1::{PublicKey, XOnlyPublicKey}, }; pub use guest_executor::io::EthClientExecutorInput; use serde::{Deserialize, Serialize}; use verifier::verify_groth16_proof; +// Regenerate these IDs after changing a leaf guest or the verifier. +// TODO: Generated by `proof-builder-rpc` +pub const EXPECTED_HEADER_CHAIN_PROGRAM_ID: verifier::ProgramId = [ + 0x87, 0x6b, 0xd8, 0x4c, 0xc8, 0x9b, 0xa4, 0x57, 0xa0, 0xfb, 0x94, 0xfc, 0x20, 0x13, 0xbd, 0x75, + 0x66, 0xae, 0xcc, 0x1e, 0x04, 0xbe, 0xfa, 0x53, 0x6c, 0x0c, 0x38, 0xd9, 0x57, 0x48, 0xf4, 0xf4, +]; +pub const EXPECTED_STATE_CHAIN_PROGRAM_ID: verifier::ProgramId = [ + 0x73, 0xc0, 0x74, 0x70, 0xf9, 0x7b, 0x3f, 0x8d, 0xe7, 0x48, 0x94, 0xad, 0x1a, 0xec, 0x60, 0xfb, + 0x82, 0x2c, 0x67, 0xf2, 0x97, 0x3a, 0x6f, 0xda, 0xef, 0x9f, 0x2b, 0xa6, 0xab, 0xb7, 0x3a, 0x17, +]; +pub const EXPECTED_COMMIT_CHAIN_PROGRAM_ID: verifier::ProgramId = [ + 0x99, 0xda, 0x42, 0x82, 0x30, 0x8f, 0x07, 0x42, 0x11, 0xd9, 0x59, 0x30, 0xd5, 0x26, 0x2c, 0xc2, + 0x4e, 0xd9, 0x13, 0x89, 0x79, 0xdf, 0x2e, 0x83, 0x59, 0xea, 0x6e, 0xc6, 0x8b, 0x91, 0xe3, 0x7d, +]; pub const GRAPH_ID_SIZE: usize = 16; pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; @@ -49,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 { @@ -64,7 +90,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, @@ -75,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, @@ -83,21 +108,16 @@ 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(), - 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); 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, @@ -105,27 +125,21 @@ 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)); - verify_groth16_proof( + let state_program_id = verify_groth16_proof( &state_chain.zkm_proof, &state_chain.zkm_public_values, &state_chain.zkm_vk_hash, &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 = @@ -137,19 +151,44 @@ pub fn watch_longest_chain( let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); - // check commit chain's genesis block let commitment = - commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); - let commitment = parse_commit_chain_commitment(&commitment); - if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment.sequencer_set_hash, x); + extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) + .expect("invalid commit-chain commitment output"); + let program_history_root = verifier::program_history_root( + checked_history_root( + verifier::ProgramType::Header, + header_program_id, + btc_header_chain_output.self_program_id, + EXPECTED_HEADER_CHAIN_PROGRAM_ID, + btc_header_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::State, + state_program_id, + state_chain_output.self_program_id, + EXPECTED_STATE_CHAIN_PROGRAM_ID, + state_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::Commit, + commit_program_id, + commit_chain_output.self_program_id, + EXPECTED_COMMIT_CHAIN_PROGRAM_ID, + commit_chain_output.program_history_hash, + ), + ); + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + assert_eq!( + commitment, + commit_chain_commitment_digest( + sequencer_set_hash, + state_chain_output.chain_state.genesis_evm_block_hash, + program_history_root, + ) + ); } else { panic!("Invalid commitment: inconsistent sequencer set hash"); }; - assert_eq!( - commitment.genesis_evm_block_hash, - state_chain_output.chain_state.genesis_evm_block_hash - ); println!("commit public inputs"); // commit public inputs @@ -239,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, @@ -248,18 +288,14 @@ 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, &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() @@ -271,18 +307,15 @@ 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, &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 @@ -377,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; @@ -421,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, @@ -429,11 +466,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 = @@ -443,19 +478,44 @@ pub fn propose_longest_chain( let commit_sequencer_set_hash = sequencer_hash(&commit_chain_output.chain_state.sequencers); let expected_seqeuencer_set_hash = cosmos_block.signed_header.header.validators_hash; - // check commit chain's genesis block let commitment = - commit_chain::extract_op_return_data(&commit_chain_output.chain_state.commit_txn.output); - let commitment = parse_commit_chain_commitment(&commitment); - if let tendermint::Hash::Sha256(x) = expected_seqeuencer_set_hash { - assert_eq!(commitment.sequencer_set_hash, x); + extract_commit_chain_commitment(&commit_chain_output.chain_state.commit_txn.output) + .expect("invalid commit-chain commitment output"); + let program_history_root = verifier::program_history_root( + checked_history_root( + verifier::ProgramType::Header, + header_program_id, + btc_header_chain_output.self_program_id, + EXPECTED_HEADER_CHAIN_PROGRAM_ID, + btc_header_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::State, + state_program_id, + state_chain_output.self_program_id, + EXPECTED_STATE_CHAIN_PROGRAM_ID, + state_chain_output.program_history_hash, + ), + checked_history_root( + verifier::ProgramType::Commit, + commit_program_id, + commit_chain_output.self_program_id, + EXPECTED_COMMIT_CHAIN_PROGRAM_ID, + commit_chain_output.program_history_hash, + ), + ); + if let tendermint::Hash::Sha256(sequencer_set_hash) = expected_seqeuencer_set_hash { + assert_eq!( + commitment, + commit_chain_commitment_digest( + sequencer_set_hash, + state_chain_output.chain_state.genesis_evm_block_hash, + program_history_root, + ) + ); } else { panic!("Invalid commitment: inconsistent sequencer set hash"); }; - assert_eq!( - commitment.genesis_evm_block_hash, - state_chain_output.chain_state.genesis_evm_block_hash - ); assert_eq!(commit_sequencer_set_hash, expected_seqeuencer_set_hash); @@ -695,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] = @@ -702,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..01f45231f 100644 --- a/crates/bitcoin-light-client-circuit/src/utils.rs +++ b/crates/bitcoin-light-client-circuit/src/utils.rs @@ -82,7 +82,7 @@ pub fn create_fee_tx( } pub fn create_sequencer_update_partial_tx( - commitment: [u8; 96], + commitment: [u8; 32], update_connector: &Option, replenish_fee_connector: &Option, next_update_connector: Address, diff --git a/crates/commit-chain/src/commit_chain.rs b/crates/commit-chain/src/commit_chain.rs index 4b8c5a087..f14c25d2a 100644 --- a/crates/commit-chain/src/commit_chain.rs +++ b/crates/commit-chain/src/commit_chain.rs @@ -9,7 +9,9 @@ pub use tendermint_light_client_verifier::{ types::{Hash, ValidatorSet}, }; +use bincode::Options as BincodeOptions; use bitcoin::{Transaction, TxOut, Witness, hashes::Hash as _, secp256k1::PublicKey}; +use sha2::{Digest, Sha256}; #[derive(Serialize, Deserialize, Debug, PartialEq)] pub struct CommitInfo { @@ -22,6 +24,8 @@ pub struct CommitInfo { pub txid: String, pub genesis_txid: String, pub sequencers: Vec, + pub genesis_evm_block_hash: [u8; 32], + pub program_history_root: [u8; 32], } /// The input proof of the commit chain circuit. @@ -29,7 +33,7 @@ pub struct CommitInfo { #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub enum CommitChainPrevProofType { GenesisBlock, - PrevProof(CommitChainCircuitOutput), + PrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -43,6 +47,8 @@ pub struct CircuitCommit { #[serde(default)] pub next_threshold: Option, pub sequencers: Vec, + pub genesis_evm_block_hash: [u8; 32], + pub program_history_root: [u8; 32], pub block_height: u32, // Bitcoin block height of current commitment } @@ -99,7 +105,6 @@ pub struct CommitChainState { pub sequencers: Vec, pub publisher_public_keys: Vec, pub threshold: u16, - pub operator_vk_hash: [u8; 32], } impl CircuitCommit { @@ -115,52 +120,14 @@ impl CircuitCommit { pub const PROOF_SIZE: usize = 260; pub const PUBLIC_INPUTS_SIZE: usize = 36; pub const VK_HASH_SIZE: usize = 66; -pub const LEGACY_COMMIT_CHAIN_COMMITMENT_SIZE: usize = 64; -pub const COMMIT_CHAIN_COMMITMENT_SIZE: usize = 96; -pub const LEGACY_OPERATOR_VK_HASH: [u8; 32] = [0u8; 32]; - -#[derive(Serialize, Deserialize, PartialEq, Eq, Clone, Debug)] -pub struct CommitChainCommitment { - pub sequencer_set_hash: [u8; 32], - pub genesis_evm_block_hash: [u8; 32], - pub operator_vk_hash: [u8; 32], -} +pub const COMMIT_CHAIN_COMMITMENT_SIZE: usize = 32; +const COMMIT_CHAIN_COMMITMENT_DOMAIN: &[u8] = b"bitvm2/commit-chain/v1"; #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct CommitChainCircuitOutput { pub chain_state: CommitChainState, -} - -#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] -struct LegacyCommitChainState { - block_height: u32, - commit_txn: Transaction, - genesis_txid: [u8; 32], - sequencers: Vec, - publisher_public_keys: Vec, - threshold: u16, -} - -#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] -struct LegacyCommitChainCircuitOutput { - chain_state: LegacyCommitChainState, -} - -impl From for CommitChainCircuitOutput { - fn from(output: LegacyCommitChainCircuitOutput) -> Self { - let chain_state = output.chain_state; - CommitChainCircuitOutput { - chain_state: CommitChainState { - block_height: chain_state.block_height, - commit_txn: chain_state.commit_txn, - genesis_txid: chain_state.genesis_txid, - sequencers: chain_state.sequencers, - publisher_public_keys: chain_state.publisher_public_keys, - threshold: chain_state.threshold, - operator_vk_hash: LEGACY_OPERATOR_VK_HASH, - }, - } - } + pub self_program_id: verifier::ProgramId, + pub program_history_hash: [u8; 32], } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -170,47 +137,83 @@ 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 deserialize_commit_chain_output(public_values).is_ok() { + return Ok(CommitChainPrevProofType::PrevProof); + } + Err("unknown commit-chain public output format".to_string()) +} + +fn deserialize_commit_chain_output( + public_values: &[u8], +) -> Result> { + bincode::DefaultOptions::new() + .with_fixint_encoding() + .reject_trailing_bytes() + .deserialize(public_values) +} + pub fn sequencer_hash(sequencers: &[SequencerInfo]) -> Hash { let sequencer_set = ValidatorSet::without_proposer(sequencers.iter().cloned().map(|s| s.into()).collect()); sequencer_set.hash() } -pub fn parse_commit_chain_commitment(commitment: &[u8]) -> CommitChainCommitment { - assert!( - commitment.len() == LEGACY_COMMIT_CHAIN_COMMITMENT_SIZE - || commitment.len() == COMMIT_CHAIN_COMMITMENT_SIZE, - "commit chain commitment must be 64 or 96 bytes" - ); - - let mut sequencer_set_hash = [0u8; 32]; - sequencer_set_hash.copy_from_slice(&commitment[0..32]); - let mut genesis_evm_block_hash = [0u8; 32]; - genesis_evm_block_hash.copy_from_slice(&commitment[32..64]); - let mut operator_vk_hash = LEGACY_OPERATOR_VK_HASH; - if commitment.len() == COMMIT_CHAIN_COMMITMENT_SIZE { - operator_vk_hash.copy_from_slice(&commitment[64..]); - assert_ne!( - operator_vk_hash, LEGACY_OPERATOR_VK_HASH, - "new commit chain commitment must include non-zero operator vk hash" - ); - } - - CommitChainCommitment { sequencer_set_hash, genesis_evm_block_hash, operator_vk_hash } +pub fn commit_chain_commitment_digest( + sequencer_set_hash: [u8; 32], + genesis_evm_block_hash: [u8; 32], + program_history_root: [u8; 32], +) -> [u8; 32] { + assert_ne!(program_history_root, [0u8; 32], "program history root must be non-zero"); + let mut hasher = Sha256::new(); + hasher.update(COMMIT_CHAIN_COMMITMENT_DOMAIN); + hasher.update(sequencer_set_hash); + hasher.update(genesis_evm_block_hash); + hasher.update(program_history_root); + hasher.finalize().into() } -/// Decode current or legacy commit-chain public values. -pub fn decode_commit_chain_circuit_output(public_values: &[u8]) -> CommitChainCircuitOutput { - if let Ok(output) = bincode::deserialize::(public_values) { - return output; +pub fn extract_commit_chain_commitment(tx_output: &[TxOut]) -> Result<[u8; 32], String> { + if tx_output.len() != 2 { + return Err(format!("commit transaction must have 2 outputs, got {}", tx_output.len())); + } + let output = &tx_output[1]; + if output.value != bitcoin::Amount::ZERO { + return Err("commitment output must have zero value".to_string()); } + let instructions = output + .script_pubkey + .instructions_minimal() + .collect::, _>>() + .map_err(|err| format!("invalid commitment script: {err}"))?; + if instructions.len() != 2 { + return Err("commitment script must be OP_RETURN followed by one push".to_string()); + } + if !matches!( + instructions[0], + bitcoin::script::Instruction::Op(op) if op == bitcoin::opcodes::all::OP_RETURN + ) { + return Err("commitment output must start with OP_RETURN".to_string()); + } + let bitcoin::script::Instruction::PushBytes(bytes) = &instructions[1] else { + return Err("commitment must be pushed bytes".to_string()); + }; + bytes + .as_bytes() + .try_into() + .map_err(|_| format!("commitment must be {COMMIT_CHAIN_COMMITMENT_SIZE} bytes")) +} - bincode::deserialize::(public_values) - .map(Into::into) - .expect("failed to decode commit chain circuit output as current or legacy format") +/// Decode current commit-chain public values. +pub fn decode_commit_chain_circuit_output(public_values: &[u8]) -> CommitChainCircuitOutput { + deserialize_commit_chain_output(public_values) + .expect("failed to decode current commit chain circuit output") } impl CommitChainState { @@ -227,7 +230,6 @@ impl CommitChainState { sequencers: Vec::new(), publisher_public_keys: vec![], threshold: u16::MAX, - operator_vk_hash: [0u8; 32], } } @@ -247,11 +249,17 @@ impl CommitChainState { } // calculate the commitment of latest sequencer set and check the equivalent - let expected_latest_commit = - extract_op_return_data(&latest_commit_txn_with_wtns.output); - let latest_commitment = parse_commit_chain_commitment(&expected_latest_commit); + let actual_commitment = + extract_commit_chain_commitment(&latest_commit_txn_with_wtns.output).unwrap(); if let Hash::Sha256(latest_sequencer_set_hash) = sequencer_hash(latest_sequencers) { - assert_eq!(latest_sequencer_set_hash, latest_commitment.sequencer_set_hash); + assert_eq!( + actual_commitment, + commit_chain_commitment_digest( + latest_sequencer_set_hash, + commit.genesis_evm_block_hash, + commit.program_history_root, + ) + ); } else { panic!("Invalid latest sequencer set hash"); } @@ -259,15 +267,8 @@ impl CommitChainState { // check the latest txn's prev out is equals to the output of prev_txn let prev_commit_txn_value = &self.commit_txn; if has_prev_commit { - // calculate the commitment of prev sequencer set and check the equivalent - let expected_prev_commit = extract_op_return_data(&prev_commit_txn_value.output); - let prev_commitment = parse_commit_chain_commitment(&expected_prev_commit); - if let Hash::Sha256(prev_sequencer_set_hash) = sequencer_hash(&self.sequencers) { - assert_eq!(prev_sequencer_set_hash, prev_commitment.sequencer_set_hash); - } else { - panic!("Invalid prev sequencer set hash"); - } - + // The recursive proof or an earlier batch item already authenticated the previous + // commitment. let update_connector = &latest_commit_txn_with_wtns.input[0]; let prev_commit_txid = prev_commit_txn_value.compute_txid(); assert_eq!(update_connector.previous_output.txid, prev_commit_txid); @@ -311,7 +312,6 @@ impl CommitChainState { self.publisher_public_keys = next_publisher_public_keys.to_vec(); self.threshold = next_threshold; self.block_height = commit.block_height; - self.operator_vk_hash = latest_commitment.operator_vk_hash; } } } @@ -333,28 +333,6 @@ pub fn extract_data_from_commitment_outputs(txouts: &[TxOut]) -> Vec { data } -pub fn extract_op_return_data(tx_output: &[TxOut]) -> Vec { - let mut results = Vec::new(); - for output in tx_output { - let script = &output.script_pubkey; - // Parse instructions from the script - let mut instructions = script.instructions(); - // First instruction should be OP_RETURN - if let Some(Ok(bitcoin::script::Instruction::Op(op))) = instructions.next() - && op == bitcoin::opcodes::all::OP_RETURN - { - // Next should be pushed data - if let Some(Ok(bitcoin::script::Instruction::PushBytes(data))) = instructions.next() { - results = data.as_bytes().to_vec(); - } - } - } - if results.is_empty() { - results = [0u8; 32].to_vec(); - } - results -} - #[cfg(test)] mod tests { use super::*; @@ -368,99 +346,120 @@ mod tests { }; #[test] fn test_extract_op_return() { - // Example: construct a fake tx with OP_RETURN - let expected_op_data = [12, 3, 4, 45]; - let script = ScriptBuf::new_op_return(expected_op_data); + let expected_op_data = [12; 32]; + let script = + ScriptBuf::new_op_return(PushBytesBuf::try_from(expected_op_data.to_vec()).unwrap()); let tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: bitcoin::absolute::LockTime::ZERO, input: vec![], - output: vec![bitcoin::TxOut { value: Amount::ZERO, script_pubkey: script }], + output: vec![ + bitcoin::TxOut { value: Amount::from_sat(500), script_pubkey: ScriptBuf::new() }, + bitcoin::TxOut { value: Amount::ZERO, script_pubkey: script }, + ], }; - let op_return_data = extract_op_return_data(&tx.output); - assert_eq!(expected_op_data.to_vec(), op_return_data); + assert_eq!(expected_op_data, extract_commit_chain_commitment(&tx.output).unwrap()); } fn commitment_payload( sequencers: &[SequencerInfo], genesis_evm_block_hash: [u8; 32], - operator_vk_hash: [u8; 32], + program_history_root: [u8; 32], ) -> PushBytesBuf { - let mut payload = Vec::with_capacity(96); - if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = - sequencer_hash(sequencers) - { - payload.extend_from_slice(&hash); - } else { - panic!("expected sha256 sequencer hash"); - }; - payload.extend_from_slice(&genesis_evm_block_hash); - payload.extend_from_slice(&operator_vk_hash); - PushBytesBuf::try_from(payload).expect("commitment payload is pushable") + let sequencer_set_hash = + if let tendermint_light_client_verifier::types::Hash::Sha256(hash) = + sequencer_hash(sequencers) + { + hash + } else { + panic!("expected sha256 sequencer hash"); + }; + PushBytesBuf::try_from( + commit_chain_commitment_digest( + sequencer_set_hash, + genesis_evm_block_hash, + program_history_root, + ) + .to_vec(), + ) + .expect("commitment payload is pushable") } #[test] - fn test_parse_commit_chain_commitment_splits_96_byte_payload() { - let sequencer_set_hash = [0x11u8; 32]; - let genesis_evm_block_hash = [0x22u8; 32]; - let operator_vk_hash = [0x33u8; 32]; - let mut payload = Vec::with_capacity(96); - payload.extend_from_slice(&sequencer_set_hash); - payload.extend_from_slice(&genesis_evm_block_hash); - payload.extend_from_slice(&operator_vk_hash); - - let commitment = parse_commit_chain_commitment(&payload); - - assert_eq!(commitment.sequencer_set_hash, sequencer_set_hash); - assert_eq!(commitment.genesis_evm_block_hash, genesis_evm_block_hash); - assert_eq!(commitment.operator_vk_hash, operator_vk_hash); + fn test_commit_chain_commitment_digest_known_vector() { + assert_eq!( + hex::encode(commit_chain_commitment_digest([0x11; 32], [0x22; 32], [0x33; 32],)), + "28c3b9bb2c89dcc9867e43e457c5232f229b340823c34a84dc6ed0e8f88147e9" + ); } #[test] - fn test_parse_commit_chain_commitment_accepts_legacy_64_byte_payload() { - let sequencer_set_hash = [0x11u8; 32]; - let genesis_evm_block_hash = [0x22u8; 32]; - let mut payload = Vec::with_capacity(64); - payload.extend_from_slice(&sequencer_set_hash); - payload.extend_from_slice(&genesis_evm_block_hash); - - let commitment = parse_commit_chain_commitment(&payload); - - assert_eq!(commitment.sequencer_set_hash, sequencer_set_hash); - assert_eq!(commitment.genesis_evm_block_hash, genesis_evm_block_hash); - assert_eq!(commitment.operator_vk_hash, LEGACY_OPERATOR_VK_HASH); + fn test_extract_commit_chain_commitment_requires_canonical_push32() { + let digest = [0x11; 32]; + let valid = vec![ + TxOut { value: Amount::from_sat(500), script_pubkey: ScriptBuf::new() }, + TxOut { + value: Amount::ZERO, + script_pubkey: ScriptBuf::new_op_return( + PushBytesBuf::try_from(digest.to_vec()).unwrap(), + ), + }, + ]; + assert_eq!(extract_commit_chain_commitment(&valid).unwrap(), digest); + + for size in [31, 33, 64, 96, 128] { + let mut outputs = valid.clone(); + outputs[1].script_pubkey = + ScriptBuf::new_op_return(PushBytesBuf::try_from(vec![0x11; size]).unwrap()); + assert!(extract_commit_chain_commitment(&outputs).is_err(), "{size} bytes"); + } + assert!(extract_commit_chain_commitment(&valid[..1]).is_err()); + let mut extra = valid.clone(); + extra.push(valid[1].clone()); + assert!(extract_commit_chain_commitment(&extra).is_err()); } #[test] - fn test_parse_commit_chain_commitment_rejects_new_payload_with_zero_operator_vk_hash() { - let mut payload = vec![0x11u8; 96]; - payload[64..].fill(0); - - let result = std::panic::catch_unwind(|| parse_commit_chain_commitment(&payload)); + #[should_panic(expected = "program history root must be non-zero")] + fn test_commit_chain_commitment_digest_rejects_zero_program_history_root() { + commit_chain_commitment_digest([0x11; 32], [0x22; 32], [0; 32]); + } - assert!(result.is_err()); + #[test] + fn test_commit_info_requires_commitment_preimage_fields() { + let legacy = serde_json::json!({ + "threshold": 2, + "publisher_public_keys": [], + "txid": "00", + "genesis_txid": "00", + "sequencers": [] + }); + assert!(serde_json::from_value::(legacy).is_err()); } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] - struct LegacyCommitChainState { + struct OldCommitChainState { block_height: u32, commit_txn: Transaction, genesis_txid: [u8; 32], sequencers: Vec, publisher_public_keys: Vec, threshold: u16, + operator_vk_hash: [u8; 32], } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] - struct LegacyCommitChainCircuitOutput { - chain_state: LegacyCommitChainState, + struct OldCommitChainCircuitOutput { + chain_state: OldCommitChainState, + self_program_id: verifier::ProgramId, + program_history_hash: [u8; 32], } #[test] - fn test_decode_commit_chain_circuit_output_accepts_legacy_public_values() { - let legacy_output = LegacyCommitChainCircuitOutput { - chain_state: LegacyCommitChainState { + fn test_classify_commit_chain_output_rejects_old_operator_vk_hash_schema() { + let old_output = OldCommitChainCircuitOutput { + chain_state: OldCommitChainState { block_height: 7, commit_txn: Transaction { version: Version::TWO, @@ -472,15 +471,19 @@ mod tests { sequencers: vec![], publisher_public_keys: vec![], threshold: 0, + operator_vk_hash: [0x33; 32], }, + self_program_id: [0x44; 32], + program_history_hash: [0x55; 32], }; - let public_values = bincode::serialize(&legacy_output).unwrap(); + let public_values = bincode::serialize(&old_output).unwrap(); - let decoded = decode_commit_chain_circuit_output(&public_values); - - assert_eq!(decoded.chain_state.block_height, legacy_output.chain_state.block_height); - assert_eq!(decoded.chain_state.genesis_txid, legacy_output.chain_state.genesis_txid); - assert_eq!(decoded.chain_state.operator_vk_hash, LEGACY_OPERATOR_VK_HASH); + assert!(bincode::deserialize::(&public_values).is_ok()); + assert!(classify_commit_chain_output(&public_values).is_err()); + assert!( + std::panic::catch_unwind(|| decode_commit_chain_circuit_output(&public_values)) + .is_err() + ); } // todo: use new commit file @@ -524,11 +527,11 @@ mod tests { let final_threshold = 4u16; let empty_sequencers = vec![]; let genesis_evm_block_hash = [0x11u8; 32]; - let operator_vk_hash = [0x22u8; 32]; + let program_history_root = [0x22u8; 32]; let commit0_op_return = ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - operator_vk_hash, + program_history_root, )); let commit0 = Transaction { version: Version::TWO, @@ -559,16 +562,18 @@ mod tests { next_publisher_public_keys: Some(current_pubkeys.clone()), next_threshold: Some(current_threshold), sequencers: empty_sequencers.clone(), + genesis_evm_block_hash, + program_history_root, block_height: 1, }; let commit1_redeem_script = create_sequencer_update_script(¤t_pubkeys, current_threshold as usize); - let commit1_operator_vk_hash = [0x33u8; 32]; + let commit1_program_history_root = [0x33u8; 32]; let commit1_op_return = ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - commit1_operator_vk_hash, + commit1_program_history_root, )); let mut commit1 = Transaction { version: Version::TWO, @@ -615,16 +620,18 @@ mod tests { next_publisher_public_keys: Some(next_pubkeys.clone()), next_threshold: Some(next_threshold), sequencers: empty_sequencers.clone(), + genesis_evm_block_hash, + program_history_root: commit1_program_history_root, block_height: 2, }; let commit2_redeem_script = create_sequencer_update_script(&next_pubkeys, next_threshold as usize); - let commit2_operator_vk_hash = [0x44u8; 32]; + let commit2_program_history_root = [0x44u8; 32]; let commit2_op_return = ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - commit2_operator_vk_hash, + commit2_program_history_root, )); let mut commit2 = Transaction { version: Version::TWO, @@ -679,6 +686,8 @@ mod tests { next_publisher_public_keys: Some(final_pubkeys.clone()), next_threshold: Some(final_threshold), sequencers: empty_sequencers, + genesis_evm_block_hash, + program_history_root: commit2_program_history_root, block_height: 3, }; @@ -686,21 +695,20 @@ mod tests { chain_state.apply_commit(vec![commit0_info]); assert_eq!(chain_state.publisher_public_keys, current_pubkeys); assert_eq!(chain_state.threshold, current_threshold); - assert_eq!(chain_state.operator_vk_hash, operator_vk_hash); chain_state.apply_commit(vec![commit1_info, commit2_info]); assert_eq!(chain_state.publisher_public_keys, final_pubkeys); assert_eq!(chain_state.threshold, final_threshold); - assert_eq!(chain_state.operator_vk_hash, commit2_operator_vk_hash); + assert_eq!(chain_state.block_height, 3); } #[test] - fn test_apply_commit_tracks_genesis_operator_vk_hash() { + fn test_apply_commit_enforces_new_genesis() { let next_keys = create_dummy_publisher_keys(3, bitcoin::Network::Regtest); let next_pubkeys: Vec = next_keys.iter().map(|(_, pk)| *pk).collect(); let empty_sequencers = vec![]; let genesis_evm_block_hash = [0x55u8; 32]; - let operator_vk_hash = [0x66u8; 32]; + let program_history_root = [0x66u8; 32]; let commit_txn = Transaction { version: Version::TWO, lock_time: LockTime::ZERO, @@ -717,7 +725,7 @@ mod tests { script_pubkey: ScriptBuf::new_op_return(commitment_payload( &empty_sequencers, genesis_evm_block_hash, - operator_vk_hash, + program_history_root, )), }, ], @@ -731,12 +739,27 @@ mod tests { next_publisher_public_keys: Some(next_pubkeys), next_threshold: Some(2), sequencers: empty_sequencers, + genesis_evm_block_hash, + program_history_root, block_height: 1, }; + let old_chain_commit = CircuitCommit { genesis_txid: [0x77; 32], ..commit.clone() }; + let result = std::panic::catch_unwind(|| { + CommitChainState::new(genesis_txid).apply_commit(vec![old_chain_commit]); + }); + assert!(result.is_err()); + + let mismatched_preimage = + CircuitCommit { program_history_root: [0x77; 32], ..commit.clone() }; + let result = std::panic::catch_unwind(|| { + CommitChainState::new(genesis_txid).apply_commit(vec![mismatched_preimage]); + }); + assert!(result.is_err()); + let mut chain_state = CommitChainState::new(genesis_txid); chain_state.apply_commit(vec![commit]); - assert_eq!(chain_state.operator_vk_hash, operator_vk_hash); + assert_eq!(chain_state.block_height, 1); } } diff --git a/crates/commit-chain/src/lib.rs b/crates/commit-chain/src/lib.rs index bee13b09c..ccec01b9b 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) - } - CommitChainPrevProofType::PrevProof(prev_proof) => { + 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,11 +20,42 @@ pub fn commit_chain_circuit(input: CommitChainCircuitInput) -> CommitChainCircui ) .unwrap(); - // todo: read from input.zkm_public_values - prev_proof.chain_state + let output = decode_commit_chain_circuit_output(&input.zkm_public_values); + assert_eq!(output.self_program_id, previous_program_id); + let history = verifier::next_history( + verifier::ProgramType::Commit, + output.program_history_hash, + previous_program_id, + self_program_id, + ); + (output.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::*; + + fn chain_state() -> CommitChainState { + CommitChainState::new([1u8; 32]) + } + + #[test] + fn classifies_only_current_outputs() { + 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 d395bb179..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"] } @@ -12,6 +13,7 @@ serde = { workspace = true, default-features = false } crypto-bigint = { version = "0.5.5", default-features = false } zkm-zkvm = { workspace = true } verifier = { workspace = true } +zkm-primitives = { workspace = true } [dev-dependencies] hex-literal = "1.0.0" diff --git a/crates/header-chain/src/header_chain.rs b/crates/header-chain/src/header_chain.rs index a36e91824..e3b081162 100644 --- a/crates/header-chain/src/header_chain.rs +++ b/crates/header-chain/src/header_chain.rs @@ -363,6 +363,13 @@ fn calculate_work(target: &[u8; 32]) -> U256 { )] pub struct BlockHeaderCircuitOutput { pub chain_state: ChainState, + pub self_program_id: verifier::ProgramId, + pub program_history_hash: [u8; 32], +} + +#[derive(Deserialize)] +struct LegacyBlockHeaderCircuitOutput { + chain_state: ChainState, } /// The input proof of the header chain circuit. @@ -370,7 +377,26 @@ pub struct BlockHeaderCircuitOutput { #[derive(Serialize, Deserialize, Eq, PartialEq, Clone, Debug, BorshDeserialize, BorshSerialize)] pub enum HeaderChainPrevProofType { GenesisBlock, - PrevProof(BlockHeaderCircuitOutput), + 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 0617922fa..bf6234204 100644 --- a/crates/header-chain/src/lib.rs +++ b/crates/header-chain/src/lib.rs @@ -17,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(), - HeaderChainPrevProofType::PrevProof(prev_proof) => { + 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, @@ -29,10 +32,67 @@ pub fn header_chain_circuit(input: HeaderChainCircuitInput) -> BlockHeaderCircui ) .unwrap(); - prev_proof.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/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..aa14ac054 100644 --- a/crates/state-chain/src/lib.rs +++ b/crates/state-chain/src/lib.rs @@ -5,16 +5,20 @@ pub use cbft::*; pub use state_chain::*; 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(prev_proof) => { + 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, @@ -22,10 +26,71 @@ pub fn state_chain_circuit(input: StateChainCircuitInput) -> StateChainCircuitOu ) .unwrap(); - prev_proof.chain_state + let state_chain_output: StateChainCircuitOutput = + 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 473db11e5..c99a19212 100644 --- a/crates/state-chain/src/state_chain.rs +++ b/crates/state-chain/src/state_chain.rs @@ -16,7 +16,8 @@ type WithdrawalSlot = (Address, [u8; 32], Vec<[u8; 16]>); #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub enum StateChainPrevProofType { GenesisBlock, - PrevProof(StateChainCircuitOutput), + PrevProof, + LegacyPrevProof, } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -42,6 +43,31 @@ pub struct StateChainState { #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] pub struct StateChainCircuitOutput { pub chain_state: StateChainState, + pub self_program_id: verifier::ProgramId, + pub program_history_hash: [u8; 32], +} + +#[derive(Deserialize)] +struct LegacyStateChainCircuitOutput { + chain_state: StateChainState, +} + +pub fn classify_state_chain_output( + public_values: &[u8], +) -> Result { + if bincode::deserialize::(public_values).is_ok() { + return Ok(StateChainPrevProofType::PrevProof); + } + if bincode::deserialize::(public_values).is_ok() { + return Ok(StateChainPrevProofType::LegacyPrevProof); + } + Err("unknown state-chain public output format".to_string()) +} + +pub fn decode_legacy_state_chain_output(public_values: &[u8]) -> Result { + bincode::deserialize::(public_values) + .map(|output| output.chain_state) + .map_err(|err| format!("invalid legacy state-chain output: {err}")) } #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] @@ -50,6 +76,7 @@ pub struct StateChainCircuitInput { pub zkm_public_values: Vec, pub zkm_vk_hash: Vec, pub zkm_version: String, + pub self_program_id: verifier::ProgramId, pub prev_proof: StateChainPrevProofType, pub blocks: Vec, } diff --git a/crates/verifier/Cargo.toml b/crates/verifier/Cargo.toml index 9201485e8..d9deba1be 100644 --- a/crates/verifier/Cargo.toml +++ b/crates/verifier/Cargo.toml @@ -4,7 +4,8 @@ version.workspace = true edition.workspace = true [dependencies] +sha2 = { workspace = true } zkm-verifier = { workspace = true } [lints] -workspace = true \ No newline at end of file +workspace = true diff --git a/crates/verifier/src/lib.rs b/crates/verifier/src/lib.rs index e08fde4c2..b024052fa 100644 --- a/crates/verifier/src/lib.rs +++ b/crates/verifier/src/lib.rs @@ -1,23 +1,104 @@ -use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES}; +use sha2::{Digest, Sha256}; +use zkm_verifier::{Groth16Verifier, IMM_GROTH16_VK_BYTES, decode_zkm_vkey_hash}; + +pub type ProgramId = [u8; 32]; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum ProgramType { + Header = 1, + State = 2, + Commit = 3, +} + +fn tagged_hash(tag: &[u8], parts: &[&[u8]]) -> [u8; 32] { + let mut hasher = Sha256::new(); + hasher.update(tag); + for part in parts { + hasher.update(part); + } + hasher.finalize().into() +} + +fn program_id_with_part_vk(zkm_vk_hash: &str, part_stark_vk: &[u8]) -> Result { + let vk_hash = decode_zkm_vkey_hash(zkm_vk_hash).map_err(|e| format!("{e:?}"))?; + let part_vk_hash: [u8; 32] = Sha256::digest(part_stark_vk).into(); + Ok(tagged_hash(b"bitvm2/program-id/v1", &[&vk_hash, &part_vk_hash])) +} + +pub fn program_id(zkm_vk_hash: &[u8], zkm_version: &str) -> Result { + let zkm_vk_hash = String::from_utf8(zkm_vk_hash.to_vec()).map_err(|e| e.to_string())?; + program_id_with_part_vk(&zkm_vk_hash, Groth16Verifier::get_part_stark_vk(zkm_version)) +} + +pub fn initial_history(program_type: ProgramType) -> [u8; 32] { + tagged_hash(b"bitvm2/vk-history-seed/v1", &[&[program_type as u8]]) +} + +pub fn legacy_history( + program_type: ProgramType, + previous_program_id: ProgramId, + previous_public_values: &[u8], +) -> [u8; 32] { + let public_values_hash: [u8; 32] = Sha256::digest(previous_public_values).into(); + tagged_hash( + b"bitvm2/vk-history-migration/v1", + &[&[program_type as u8], &previous_program_id, &public_values_hash], + ) +} + +pub fn next_history( + program_type: ProgramType, + previous_history: [u8; 32], + previous_program_id: ProgramId, + current_program_id: ProgramId, +) -> [u8; 32] { + if previous_program_id == current_program_id { + previous_history + } else { + tagged_hash( + b"bitvm2/vk-history-step/v1", + &[&[program_type as u8], &previous_history, &previous_program_id], + ) + } +} + +pub fn finalize_history( + program_type: ProgramType, + history: [u8; 32], + current_program_id: ProgramId, +) -> [u8; 32] { + tagged_hash( + b"bitvm2/vk-history-final/v1", + &[&[program_type as u8], &history, ¤t_program_id], + ) +} + +pub fn program_history_root( + header_history: [u8; 32], + state_history: [u8; 32], + commit_history: [u8; 32], +) -> [u8; 32] { + tagged_hash(b"bitvm2/program-history/v1", &[&header_history, &state_history, &commit_history]) +} pub fn verify_groth16_proof( proof: &[u8], zkm_public_values: &[u8], zkm_vk_hash: &[u8], zkm_version: &str, -) -> Result<(), String> { +) -> Result { let groth16_vk = *IMM_GROTH16_VK_BYTES; let zkm_vk_hash = String::from_utf8(zkm_vk_hash.to_vec()).map_err(|e| e.to_string())?; let part_stark_vk = Groth16Verifier::get_part_stark_vk(zkm_version); - match Groth16Verifier::verify_by_imm_groth16_vk( + Groth16Verifier::verify_by_imm_groth16_vk( proof, zkm_public_values, &zkm_vk_hash, groth16_vk, part_stark_vk, - ) { - Ok(_) => Ok(()), - Err(err) => Err(format!("Verify Groth16 proof, err: {err:?}")), - } + ) + .map_err(|err| format!("Verify Groth16 proof, err: {err:?}"))?; + + program_id_with_part_vk(&zkm_vk_hash, part_stark_vk) } diff --git a/node/Cargo.toml b/node/Cargo.toml index 38ad959ad..650a031b6 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -132,4 +132,7 @@ store = { workspace = true } client = { workspace = true } bitcoin-light-client-circuit = { workspace = true } commit-chain = { workspace = true } +header-chain = { workspace = true } +state-chain = { workspace = true } +verifier = { workspace = true } cbft-rpc = { workspace = true } diff --git a/node/src/bin/sequencer-set-publish.rs b/node/src/bin/sequencer-set-publish.rs index f715eddfb..bbccdd78c 100644 --- a/node/src/bin/sequencer-set-publish.rs +++ b/node/src/bin/sequencer-set-publish.rs @@ -36,7 +36,16 @@ use bitcoin_light_client_circuit::{ /*create_dummy_publisher_keys,*/ create_fee_tx, create_sequencer_update_partial_tx, estimate_tx_vbytes, }; -use commit_chain::{CommitInfo, create_sequencer_update_script, finalize, sign_raw}; +use commit_chain::{ + CommitChainCircuitOutput, CommitChainPrevProofType, classify_commit_chain_output, +}; +use commit_chain::{ + CommitInfo, commit_chain_commitment_digest, 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; @@ -108,6 +117,7 @@ impl OutputData { } } +#[allow(clippy::too_many_arguments)] async fn save_commit_info( output_file: &str, btc_public_keys: &[secp256k1::PublicKey], @@ -115,6 +125,8 @@ async fn save_commit_info( sequencers: Vec, init_genesis: bool, commit_info_file: &str, + genesis_evm_block_hash: [u8; 32], + program_history_root: [u8; 32], ) -> Result<(), Box> { let file = std::fs::File::open(output_file)?; let output: OutputData = serde_json::from_reader(file).unwrap(); @@ -136,6 +148,8 @@ async fn save_commit_info( ), genesis_txid, sequencers: sequencers.iter().cloned().map(|v| v.into()).collect(), + genesis_evm_block_hash, + program_history_root, }; let commit_info = serde_json::to_string(&commit_info).unwrap(); @@ -209,6 +223,26 @@ 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, + required_unless_present = "commit_chain_genesis", + conflicts_with = "commit_chain_genesis" + )] + commit_chain_input_proof: Option, + #[arg(long, default_value_t = false, conflicts_with = "commit_chain_input_proof")] + commit_chain_genesis: bool, + #[arg(long, value_parser = hex_parse::<32>)] + next_header_program_id: [u8; 32], + #[arg(long, value_parser = hex_parse::<32>)] + next_state_program_id: [u8; 32], + #[arg(long, value_parser = hex_parse::<32>)] + next_commit_program_id: [u8; 32], + }, Pubkey { #[arg(long, short, value_delimiter = ',')] btc_key_wifs: Vec, @@ -230,8 +264,8 @@ enum Commands { next_publisher_btc_pubkeys: Vec, #[arg(long, env = "GOAT_GENESIS_BLOCK_HASH", value_parser = hex_parse::<32>)] goat_genesis_block_hash: [u8; 32], - #[arg(long, env = "OPERATOR_VK_HASH", value_parser = hex_parse::<32>)] - operator_vk_hash: [u8; 32], + #[arg(long, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] + program_history_root: [u8; 32], }, PushSeq { #[arg(long, env = "OWNER_BTC_KEY_WIF")] @@ -246,8 +280,8 @@ enum Commands { init_genesis: bool, #[arg(long, env = "GOAT_GENESIS_BLOCK_HASH", value_parser = hex_parse::<32>)] goat_genesis_block_hash: [u8; 32], - #[arg(long, env = "OPERATOR_VK_HASH", value_parser = hex_parse::<32>)] - operator_vk_hash: [u8; 32], + #[arg(long, env = "PROGRAM_HISTORY_ROOT", value_parser = hex_parse::<32>)] + program_history_root: [u8; 32], #[arg(long)] commit_info: String, }, @@ -274,6 +308,28 @@ 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, + commit_chain_genesis, + 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.as_deref(), + *commit_chain_genesis, + *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 +343,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| { @@ -336,7 +393,7 @@ async fn main() -> Result<(), Box> { publisher_btc_pubkeys, next_publisher_btc_pubkeys, goat_genesis_block_hash, - operator_vk_hash, + program_history_root, } => { let (sequencer_set_hash, goat_block_number, cosmos_block_number) = get_sequencer_set_hash_from_db(&args.db_path, goat_block_number, false).await?; @@ -357,7 +414,7 @@ async fn main() -> Result<(), Box> { update_connector, sequencer_set_hash, goat_genesis_block_hash, - operator_vk_hash, + program_history_root, goat_block_number, ) .await @@ -369,7 +426,7 @@ async fn main() -> Result<(), Box> { next_publisher_btc_pubkeys, 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)); @@ -392,30 +449,147 @@ async fn main() -> Result<(), Box> { goat_block_number, sequencer_set_hash, goat_genesis_block_hash, - operator_vk_hash, + program_history_root, output_file, ) .await?; - match save_commit_info( + save_commit_info( &args.output_file, &publisher_btc_pubkeys, &next_publisher_btc_pubkeys, sequencers, init_genesis, &commit_info, + goat_genesis_block_hash, + program_history_root, ) - .await - { - Err(e) => { - println!("Failed to save commit info: {e}, commit_info: {commit_info}"); - Ok(()) - } - _ => Ok(()), - } + .await?; + Ok(()) } } } +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::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: Option<&str>, + commit_chain_genesis: bool, + next_header_program_id: verifier::ProgramId, + next_state_program_id: verifier::ProgramId, + next_commit_program_id: verifier::ProgramId, +) -> anyhow::Result<[u8; 32]> { + let commit_history_root = if commit_chain_genesis { + verifier::finalize_history( + verifier::ProgramType::Commit, + verifier::initial_history(verifier::ProgramType::Commit), + next_commit_program_id, + ) + } else { + next_commit_history_root( + commit_path.expect("commit proof is required unless genesis is selected"), + next_commit_program_id, + )? + }; + Ok(verifier::program_history_root( + next_header_history_root(header_path, next_header_program_id)?, + next_state_history_root(state_path, next_state_program_id)?, + commit_history_root, + )) +} + async fn push_fee_tx( fee_tx: &mut Transaction, input_value: Amount, @@ -545,7 +719,7 @@ async fn action_push_sequencer_set_update( goat_block_number: u64, sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], - operator_vk_hash: [u8; 32], + 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 +780,11 @@ async fn action_push_sequencer_set_update( }; // Skip construction of the genesis tx - let mut commitment = [0u8; 96]; - commitment[0..32].copy_from_slice(&sequencer_set_hash); - commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..].copy_from_slice(&operator_vk_hash); + let commitment = commit_chain_commitment_digest( + sequencer_set_hash, + goat_genesis_block_hash, + program_history_root, + ); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, &update_connector, @@ -655,7 +830,7 @@ async fn action_sign_sequencer_set_update( update_connector: Option, sequencer_set_hash: [u8; 32], goat_genesis_block_hash: [u8; 32], - operator_vk_hash: [u8; 32], + program_history_root: [u8; 32], goat_block_number: u64, ) -> Result<(), Box> { let total = btc_public_keys.len(); @@ -672,10 +847,11 @@ async fn action_sign_sequencer_set_update( * estimate_tx_vbytes(&[(threshold as u32, total as u32)], &[("p2wsh", 3)], 73) as f64 + RELAYER_FEE as f64; let replenish_fee = Amount::from_sat(replenish_fee.ceil() as u64); - let mut commitment = [0u8; 96]; - commitment[0..32].copy_from_slice(&sequencer_set_hash); - commitment[32..64].copy_from_slice(&goat_genesis_block_hash); - commitment[64..].copy_from_slice(&operator_vk_hash); + let commitment = commit_chain_commitment_digest( + sequencer_set_hash, + goat_genesis_block_hash, + program_history_root, + ); let mut sequencer_set_publish_tx = create_sequencer_update_partial_tx( commitment, diff --git a/node/src/handle.rs b/node/src/handle.rs index 3ed9c50be..750c5d2d7 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -1801,15 +1801,7 @@ async fn handle_soldering_proof_ready_operator( total_len, payload_hash = %soldering_payload_hash_hex(&payload_hash), payload_path = %payload_path, - "read soldering proof payload from store" - ); - tracing::info!( - instance_id = %instance_id, - graph_id = %graph_id, - verifier_index, - total_len, - payload_hash = %soldering_payload_hash_hex(&payload_hash), - "start processing soldering proof payload" + "read soldering proof payload from store, start processing" ); handle_soldering_proof_payload_operator(ctx, &soldering_proof_ready, &payload).await } 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)?) } diff --git a/node/ssp-ci.sh b/node/ssp-ci.sh index 90f035e77..9f856c65c 100644 --- a/node/ssp-ci.sh +++ b/node/ssp-ci.sh @@ -26,16 +26,16 @@ $CMD fund $CMD payfee --total 5 echo -e "publish genisis sign sequencer set" -$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH --init-genesis --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.0" +$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --init-genesis --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.0" echo -e "set the new publisher set: publishers: ${PUBLISHER_BTC_PUBKEYS} => next_publishers: ${NEXT_PUBLISHER_BTC_PUBKEYS}" $CMD payfee --total 5 -$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH -$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH -$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH -$CMD sign-seq --owner-btc-key-wif cMiWPrRA5KYDiRAq4nkgGsEf2TfcpqGbhT6YbfDpoy8ZsaAHiDeo --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH -$CMD sign-seq --owner-btc-key-wif cMkTafzStDS4RMRPYD7Emw9DfN5Yendp9R9eKBaNg7tBWwGU43fD --goat-block-number $GOAT_BLOCK_NUMBER --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER +$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER +$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER +$CMD sign-seq --owner-btc-key-wif cMiWPrRA5KYDiRAq4nkgGsEf2TfcpqGbhT6YbfDpoy8ZsaAHiDeo --goat-block-number $GOAT_BLOCK_NUMBER +$CMD sign-seq --owner-btc-key-wif cMkTafzStDS4RMRPYD7Emw9DfN5Yendp9R9eKBaNg7tBWwGU43fD --goat-block-number $GOAT_BLOCK_NUMBER # broadcast publisher changes to Bitcoin $CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.1" @@ -44,10 +44,10 @@ GOAT_BLOCK_NUMBER=$(($GOAT_BLOCK_NUMBER+1)) echo -e "recover the publisher set: next_publishers => publishers" $CMD payfee --total 3 -$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH -$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH -$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH +$CMD sign-seq --owner-btc-key-wif cMceqPhHedrhbcR9eXgzmfWy7kRqLyAxMYwFT6ABDWsiwUp9Nsq9 --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS +$CMD sign-seq --owner-btc-key-wif cMec2DGaTXkYJYfi7x3ZGjRXkeqmAvYAoWzMAcWj5fdLaqudWsNi --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS +$CMD sign-seq --owner-btc-key-wif cMgZD2qsGReP1UvGbNQ7moL6PZFgzsuPFV3St8sGwpNxED4hqkEM --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS # broadcast publisher changes to Bitcoin -$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --operator-vk-hash $OPERATOR_VK_HASH --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.2" +$CMD push-seq --goat-block-number $GOAT_BLOCK_NUMBER --next-publisher-btc-pubkeys=$PUBLISHER_BTC_PUBKEYS --publisher-btc-pubkeys=$NEXT_PUBLISHER_BTC_PUBKEYS --commit-info="${DIR}/../circuits/data/commit-chain/commit_info.json.2"