diff --git a/Cargo.lock b/Cargo.lock index 2f6a46132..66f2c4527 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5576,7 +5576,7 @@ dependencies = [ [[package]] name = "garbled-snark-verifier" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12226,7 +12226,7 @@ dependencies = [ [[package]] name = "soldering-host" version = "1.1.0" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -12236,6 +12236,7 @@ dependencies = [ "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=GA)", "garbled-snark-verifier", "rand 0.8.5", + "rand_chacha 0.3.1", "tracing", "verifiable-circuit-babe", "zkm-build", @@ -13911,7 +13912,7 @@ dependencies = [ [[package]] name = "verifiable-circuit-babe" version = "0.0.1" -source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fbabe#834d4337341e14da569c8c98c9fff546c9aedab6" +source = "git+https://github.com/GOATNetwork/bitvm2-gc?branch=feat%2Fgoat-bitvm3#a57d6df9f1f5bc782af306170886e14838552258" dependencies = [ "aes", "ark-bn254", @@ -13930,9 +13931,11 @@ dependencies = [ "p3-maybe-rayon", "rand 0.8.5", "rand_chacha 0.3.1", + "rayon", "ripemd 0.2.0", "serde", "sha2 0.10.9", + "tracing", "zkm-sdk", "zkm-zkvm", ] diff --git a/Cargo.toml b/Cargo.toml index f8aab0cec..340b22a4a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,9 +107,13 @@ p3-field = { git = "https://github.com/ProjectZKM/Plonky3" } #zkm-verifier = { path = "../Ziren/crates/verifier" } bitvm-lib = { package = "bitvm-gc", path = "crates/bitvm-gc" } -verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } -garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } -soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/babe" } +verifiable-circuit-babe = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +garbled-snark-verifier = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } +soldering-host = { git = "https://github.com/GOATNetwork/bitvm2-gc", branch = "feat/goat-bitvm3" } + +#verifiable-circuit-babe = { path = "../bitvm2-gc/verifiable-circuit-babe"} +#garbled-snark-verifier = { path = "../bitvm2-gc/garbled-snark-verifier"} +#soldering-host = { path = "../bitvm2-gc/babe-programs/soldering/host" } store = { path = "crates/store" } util = { path = "crates/util" } client = { path = "crates/client" } diff --git a/crates/bitvm-gc/src/babe_adapter.rs b/crates/bitvm-gc/src/babe_adapter.rs index ccce0658a..0f34a8680 100644 --- a/crates/bitvm-gc/src/babe_adapter.rs +++ b/crates/bitvm-gc/src/babe_adapter.rs @@ -1,12 +1,12 @@ -use rayon::prelude::*; use std::collections::HashSet; use std::panic::{AssertUnwindSafe, catch_unwind}; use std::path::PathBuf; use anyhow::{Result, bail}; -use ark_bn254::{Bn254, Fq, Fr}; +use ark_bn254::{Bn254, Fr}; +use ark_bn254::g1::G1Affine; use ark_groth16::VerifyingKey as Groth16VerifyingKey; -use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; +use ark_serialize::CanonicalSerialize; use garbled_snark_verifier::bag::S; use goat::assert_scripts::{ INPUT_WIRE_NUM, OperatorAssertPublicKey, OperatorAssertSecretKey, WireHash, label_hash, @@ -16,25 +16,17 @@ use serde::{Deserialize, Serialize}; use sha2::{Digest, Sha256}; use soldering_host::BabeBundle; pub use soldering_host::BabeBundleBuilder; -use verifiable_circuit_babe::babe::{ - LAMPORT_N, ProverSetupState, WeKnownPi1SetupCt as RealSetupCt, babe_verifier_presign, -}; -use verifiable_circuit_babe::cac::{ - CACSetupPackage as RealCACSetupPackage, FinalizedInstanceData as RealFinalizedInstanceData, - cac_finalize_indices, -}; -use verifiable_circuit_babe::gc::{ - SparseAdaptorEntry as RealSparseAdaptorEntry, SparseAdaptorRow as RealSparseAdaptorRow, - SparseAdaptorTable as RealSparseAdaptorTable, -}; -use verifiable_circuit_babe::instance::BABEInstance; -use verifiable_circuit_babe::instance::commit::CACInstanceCommit as RealCACInstanceCommit; +use verifiable_circuit_babe::babe::{WeKnownPi1SetupCt, GC_INPUT_WIRES, build_challenge_assert_witness_raw, deinterleave_dummy_positions, interleave_dummy_positions}; +pub use verifiable_circuit_babe::cac::{CACSetupPackage, FinalizedInstanceData}; +use verifiable_circuit_babe::cac::cac_finalize_indices; +use verifiable_circuit_babe::gc::{SparseAdaptorTable, SGC_PART1_CONSTANT_SIZE}; +pub use verifiable_circuit_babe::instance::commit::CACInstanceCommit; use verifiable_circuit_babe::prover::BABEProver; -use verifiable_circuit_babe::soldering::{ - SolderingData as RealSolderingData, SolderingProof as RealSolderingProof, -}; +use verifiable_circuit_babe::soldering::{SolderedLabelsData, SolderingData as RealSolderingData, SolderingProof as RealSolderingProof}; +pub use verifiable_circuit_babe::transactions::TxAssertWitness; +pub use verifiable_circuit_babe::transactions::ChallengeAssertWitnessRaw; +use verifiable_circuit_babe::utils::pi1_xd_to_wots96_msg; use verifiable_circuit_babe::verifier::BABEVerifier; - use crate::types::BitvmGcCircuitData; /// Number of Wots96 digit signatures expected by the GOAT GC-V2 connector. @@ -47,48 +39,6 @@ pub type OpenedInstanceSeeds = Vec<(usize, u64)>; pub type FinalizedInstances = Vec; pub type SetupAndSolderingData = (OpenedInstanceSeeds, FinalizedInstances, SolderingData); -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CACSetupPackage { - pub commits: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CACInstanceCommit { - pub epk: Vec<[[u8; 20]; 2]>, - pub wots_padding_epk: [[[u8; 20]; 2]; 4], - pub constant_commits: [[[u8; 32]; 2]; 2], - pub h_msg: [u8; 20], - pub h_ct_setup: [u8; 32], - pub com_adaptor: [u8; 32], - pub com_gc: [u8; 32], -} - -#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct FinalizedInstanceData { - pub index: usize, - pub final_msg_hash: [u8; 20], - pub wire_hashes: Vec, - pub gc_commitment: [u8; 32], - pub adaptor_commitment: [u8; 32], - pub ct_setup_commitment: [u8; 32], - pub real_data: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct RealFinalizedPayload { - pub gc_ciphertexts: Vec>, - pub adaptor_table: SerializableSparseAdaptorTable, - pub ct_setup: SerializableSetupCt, - pub constant_labels: [[u8; 16]; 2], - pub wots_padding_zero_labels: [[u8; 16]; 4], -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct CompactFinalizedInstanceData { - pub index: usize, - pub real_data: RealFinalizedPayload, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactSolderingData { pub finalized_indices: Vec, @@ -98,33 +48,10 @@ pub struct CompactSolderingData { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct CompactSolderingProofPayload { pub opened: OpenedInstanceSeeds, - pub finalized: Vec, + pub finalized: Vec, pub soldering: CompactSolderingData, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSetupCt { - pub ct2_r_delta_g2: Vec, - pub ct3_masked_msg: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorTable { - pub entries: Vec, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorEntry { - pub x: SerializableSparseAdaptorRow, - pub y: SerializableSparseAdaptorRow, - pub z: SerializableSparseAdaptorRow, -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct SerializableSparseAdaptorRow { - pub cts: Vec<[u8; 32]>, - pub offset: Vec, -} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct SolderingData { @@ -133,17 +60,10 @@ pub struct SolderingData { pub proof: Vec, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)] -pub struct SolderedLabelsData { - pub base_commitment: Vec<([u8; 32], [u8; 32])>, - pub deltas: Vec>, - pub commitments: Vec>, -} #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeVerifierPrivateState { pub instance_seeds: Vec, - pub temp_val: [u8; 32], pub statement_digest: [u8; 32], } @@ -162,79 +82,79 @@ pub struct BabeProverState { pub h_msgs: Vec<[u8; 20]>, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct BabeAssertWitness { - pub pi1: Vec, - #[serde(default)] - pub pubin_commitment: [u8; 32], - pub wots_sig: Vec<[u8; 21]>, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeChallengeAssertWitness { pub verifier_index: usize, - pub input_labels: Vec<[u8; 16]>, - pub wots_sig: Vec<[u8; 21]>, + pub witness: ChallengeAssertWitnessRaw, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct BabeWronglyChallengedWitness { pub verifier_index: usize, - pub final_msgs: Vec>, + // just need to contain one valid msg + pub final_msg: Vec, } -impl CACInstanceCommit { - /// Builds deterministic placeholder setup commitments for tests and wiring. - pub fn sample(seed: u8) -> Self { - let epk = (0..LAMPORT_N) - .map(|wire| [hash20(&[seed, wire as u8, 0]), hash20(&[seed, wire as u8, 1])]) - .collect(); - Self { - epk, - wots_padding_epk: padding_wire_hashes(), - constant_commits: [ - [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])], - [hash32(&[seed, 0xf1, 0]), hash32(&[seed, 0xf1, 1])], - ], - h_msg: hash20(&[seed, 0xa0]), - h_ct_setup: hash32(&[seed, 0xa1]), - com_adaptor: hash32(&[seed, 0xa2]), - com_gc: hash32(&[seed, 0xa3]), - } - } -} -impl FinalizedInstanceData { - /// Builds deterministic placeholder finalized data for tests and graph wiring. - pub fn sample(index: usize) -> Self { - let seed = index as u8; - let wire_hashes = (0..INPUT_WIRE_NUM) - .map(|wire| WireHash { - true_label_hash: hash20(&[seed, wire as u8, 1]), - false_label_hash: hash20(&[seed, wire as u8, 0]), - }) - .collect(); - Self { - index, - final_msg_hash: hash20(&[seed, 0xb0]), - wire_hashes, - gc_commitment: hash32(&[seed, 0xb1]), - adaptor_commitment: hash32(&[seed, 0xb2]), - ct_setup_commitment: hash32(&[seed, 0xb3]), - real_data: None, +/// Builds deterministic placeholder setup commitments for tests and wiring. +pub fn sample_cac_instance_commit(seed: u8) -> CACInstanceCommit { + let epk = (0..GC_INPUT_WIRES) + .map(|wire| { + let w = (wire as u16).to_le_bytes(); + [hash20(&[seed, w[0], w[1], 0]), hash20(&[seed, w[0], w[1], 1])] + }) + .collect(); + + let constant_commit_sgc: Vec<_> = (0..SGC_PART1_CONSTANT_SIZE) + .map(|_| { + [ + hash32(&[seed, 0xf0, 0]), + hash32(&[seed, 0xf0, 1]), + ] + }) + .collect(); + + CACInstanceCommit { + epk, + constant_commits_fgc: [ + [hash32(&[seed, 0xf0, 0]), hash32(&[seed, 0xf0, 1])], + [hash32(&[seed, 0xf1, 0]), hash32(&[seed, 0xf1, 1])], + ], + constant_commits_sgc: constant_commit_sgc, + b_blind_commit: hash32(&[seed, 0xa0]), + h_msg: hash20(&[seed, 0xa1]), + h_ct_setup: hash32(&[seed, 0xa2]), + com_adaptor: [hash32(&[seed, 0xa3]), hash32(&[seed, 0xa4])], + com_gc: [hash32(&[seed, 0xa5]), hash32(&[seed, 0xa6]), hash32(&[seed, 0xa7])], + } +} + +pub fn sample_finalized_instance_data(index: usize) -> FinalizedInstanceData { + let adaptor_tables = [ + SparseAdaptorTable { + entries: vec![] + }, + SparseAdaptorTable { + entries: vec![] } - } -} + ]; + let ct_setup = WeKnownPi1SetupCt { + ct2_r_delta_g2: vec![], + ct3_masked_msg: vec![], + }; -impl CompactFinalizedInstanceData { - pub fn try_from_finalized(finalized: &FinalizedInstanceData) -> Result { - let real_data = finalized.real_data.clone().ok_or_else(|| { - anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) - })?; - Ok(Self { index: finalized.index, real_data }) + FinalizedInstanceData { + index, + ciphertext_sets: [vec![], vec![], vec![]], + adaptor_tables, + ct_setup, + constant_labels_0: [S([0; 16]), S([0; 16])], + constant_labels_1: vec![], + b: G1Affine::identity(), } } + impl SolderingData { /// Builds deterministic placeholder soldering data for the selected finalized indices. pub fn sample(finalized_indices: Vec) -> Self { @@ -248,7 +168,7 @@ pub fn build_setup_package(n_cc: usize) -> Result { bail!("n_cc must be greater than zero"); } Ok(CACSetupPackage { - commits: (0..n_cc).map(|index| CACInstanceCommit::sample(index as u8)).collect(), + commits: (0..n_cc).map(|index| sample_cac_instance_commit(index as u8)).collect(), }) } @@ -256,20 +176,19 @@ pub fn build_setup_package(n_cc: usize) -> Result { pub fn build_real_setup_package( n_cc: usize, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result<(CACSetupPackage, BabeVerifierPrivateState)> { if n_cc == 0 { bail!("n_cc must be greater than zero"); } ensure_real_gc_assets_configured()?; - let verifier = catch_unwind(AssertUnwindSafe(|| BABEVerifier::new(n_cc, vk, public_inputs))) + let verifier = catch_unwind(AssertUnwindSafe(|| BABEVerifier::new(n_cc, vk, static_inputs))) .map_err(|_| anyhow::anyhow!("real BABE verifier setup panicked while loading GC assets"))? .map_err(anyhow::Error::msg)?; - let package = from_real_package(&verifier.commit()); + let package = verifier.commit(); let private_state = BabeVerifierPrivateState { - instance_seeds: verifier.instances.iter().map(|instance| instance.seed).collect(), - temp_val: verifier.temp_val, - statement_digest: statement_digest(vk, public_inputs)?, + instance_seeds: verifier.get_seeds(), + statement_digest: statement_digest(vk, static_inputs)?, }; Ok((package, private_state)) @@ -282,15 +201,15 @@ pub fn open_real_setup_and_solder( package: &CACSetupPackage, finalized_indices: &[usize], vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result { ensure_real_gc_assets_configured()?; - if private_state.statement_digest != statement_digest(vk, public_inputs)? { + if private_state.statement_digest != statement_digest(vk, static_inputs)? { bail!("BABE setup statement does not match persisted verifier state"); } validate_finalized_indices(package, finalized_indices)?; - let verifier = restore_real_verifier(private_state, vk, public_inputs)?; - if from_real_package(&verifier.commit()) != *package { + let verifier = restore_real_verifier(private_state, package, vk, static_inputs)?; + if verifier.commit() != *package { bail!("persisted BABE verifier state does not reproduce setup package"); } let bundle = soldering_builder @@ -298,11 +217,7 @@ pub fn open_real_setup_and_solder( .map_err(anyhow::Error::msg)?; Ok(( bundle.opened, - bundle - .finalized - .iter() - .map(|data| from_real_finalized(data, package)) - .collect::>>()?, + bundle.finalized, from_real_soldering(&bundle.soldering)?, )) } @@ -315,22 +230,15 @@ pub fn verify_real_setup( finalized: &[FinalizedInstanceData], soldering: &SolderingData, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_public_inputs: Fr, ) -> Result<()> { - let real_package = to_real_package(package); - let real_finalized = finalized - .iter() - .map(to_real_finalized) - .collect::>>()?; - let bundle = BabeBundle { opened: opened.to_vec(), - finalized: real_finalized, + finalized: finalized.to_vec(), soldering: to_real_soldering(soldering)?, - temp_hashlock: [0u8; 20], }; soldering_builder - .babe_prover_verify_setup(&real_package, &bundle, vk, public_inputs) + .babe_prover_verify_setup(&package, &bundle, vk, static_public_inputs) .map_err(anyhow::Error::msg) } @@ -342,10 +250,7 @@ pub fn compact_soldering_proof_payload( ) -> Result { Ok(CompactSolderingProofPayload { opened: opened.to_vec(), - finalized: finalized - .iter() - .map(CompactFinalizedInstanceData::try_from_finalized) - .collect::>>()?, + finalized: finalized.to_vec(), soldering: CompactSolderingData { finalized_indices: soldering.finalized_indices.clone(), proof: soldering.proof.clone(), @@ -355,14 +260,9 @@ pub fn compact_soldering_proof_payload( /// Reconstructs the full BABE setup data using the locally trusted setup package. pub fn expand_compact_soldering_proof_payload( - package: &CACSetupPackage, payload: CompactSolderingProofPayload, ) -> Result { - let finalized = payload - .finalized - .into_iter() - .map(|data| expand_compact_finalized_instance(package, data)) - .collect::>>()?; + let finalized = payload.finalized.clone(); Ok((payload.opened, finalized, expand_compact_soldering_data(payload.soldering)?)) } @@ -372,7 +272,7 @@ pub fn derive_finalized_indices(package: &CACSetupPackage, m_cc: usize) -> Resul if m_cc == 0 || m_cc > n_cc { bail!("invalid m_cc {m_cc} for n_cc {n_cc}"); } - Ok(cac_finalize_indices(&to_real_package(package), m_cc)) + Ok(cac_finalize_indices(n_cc, m_cc)) } /// Opens non-finalized placeholder instances and returns finalized data plus soldering data. @@ -392,7 +292,7 @@ pub fn open_and_solder( .map(|index| (index, deterministic_seed(index))) .collect::>(); let finalized = - finalized_indices.iter().map(|index| FinalizedInstanceData::sample(*index)).collect(); + finalized_indices.iter().map(|index| sample_finalized_instance_data(*index)).collect(); let soldering = SolderingData::sample(finalized_indices.to_vec()); Ok((opened, finalized, soldering)) } @@ -427,36 +327,44 @@ pub fn verify_setup( if data.index >= n_cc { bail!("finalized index {} out of range", data.index); } - if data.wire_hashes.len() != INPUT_WIRE_NUM { - bail!("finalized index {} has invalid wire hash count", data.index); - } + } Ok(()) } /// Extracts one graph slot owned by `verifier_pubkey` from finalized setup data. +/// +/// `epk` must have exactly `GC_INPUT_WIRES` (762) entries pub fn extract_gc_circuit_data( - finalized: &[FinalizedInstanceData], - soldering: &SolderingData, verifier_pubkey: bitcoin::PublicKey, + epk: &[[[u8; 20]; 2]], + h_msgs: &[[u8; 20]], ) -> Result { - if finalized.len() != BABE_M_CC { - bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); + if epk.len() != GC_INPUT_WIRES { + bail!("BABE epk has {} entries; expected {GC_INPUT_WIRES}", epk.len()); } - if soldering.finalized_indices != finalized.iter().map(|data| data.index).collect::>() { - bail!("soldering finalized indices mismatch"); - } - let data = &finalized[0]; - let wire_hashes: [WireHash; INPUT_WIRE_NUM] = - data.wire_hashes.clone().try_into().map_err(|wire_hashes: Vec| { - anyhow::anyhow!( - "BABE input label count {} is incompatible with GOAT verifier connector wire count {INPUT_WIRE_NUM}", - wire_hashes.len() - ) - })?; + let n = GC_INPUT_WIRES / 3; + let to_wire_hash = |pair: &[[u8; 20]; 2]| WireHash { + false_label_hash: pair[0], + true_label_hash: pair[1], + }; + let dummy_hash = label_hash(&vec![0u8; 16]); + let dummy = WireHash { false_label_hash: dummy_hash, true_label_hash: dummy_hash }; + + let pi1_x: Vec = epk[..n].iter().map(to_wire_hash).collect(); + let pi1_y: Vec = epk[n..2 * n].iter().map(to_wire_hash).collect(); + let x_d: Vec = epk[2 * n..].iter().map(to_wire_hash).collect(); + let wire_hashes_vec = interleave_dummy_positions(&pi1_x, &pi1_y, &x_d, dummy); + + let wire_hashes: [WireHash; INPUT_WIRE_NUM] = wire_hashes_vec.try_into().map_err( + |v: Vec| anyhow::anyhow!( + "wire hash count {} does not match expected {INPUT_WIRE_NUM}", + v.len() + ), + )?; Ok(BitvmGcCircuitData { verifier_pubkey, - final_msg_hashlocks: finalized.iter().map(|data| data.final_msg_hash).collect(), + final_msg_hashlocks: h_msgs.to_vec(), wire_hashes, }) } @@ -465,54 +373,43 @@ pub fn extract_gc_circuit_data( pub fn build_assert_witness( proof: &ark_groth16::Proof, assert_secret_key: &OperatorAssertSecretKey, -) -> Result { - build_assert_witness_with_pubin_commitment(proof, &[0u8; 32], assert_secret_key) -} - -pub fn build_assert_witness_with_pubin_commitment( - proof: &ark_groth16::Proof, - pubin_commitment: &[u8; 32], - assert_secret_key: &OperatorAssertSecretKey, -) -> Result { + dynamic_input: ark_bn254::Fr, +) -> Result { if assert_secret_key.is_empty() { bail!("operator WOTS secret key must not be empty"); } - let pi1 = proof.a; - let mut pi1_bytes = Vec::new(); - pi1.serialize_compressed(&mut pi1_bytes).expect("serialize pi1"); - let msg = pi1_to_wots96_msg(&pi1, pubin_commitment); + let msg = pi1_xd_to_wots96_msg(&proof.a, dynamic_input); let wots_sig = Wots96::sign(assert_secret_key, &msg); - Ok(BabeAssertWitness { - pi1: pi1_bytes, - pubin_commitment: *pubin_commitment, - wots_sig: wots_sig.to_vec(), - }) + Ok(TxAssertWitness { wots_sig: wots_sig.to_vec() }) } -pub fn assert_wots_message(assert_witness: &BabeAssertWitness) -> Result<[u8; 96]> { - let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) - .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; - Ok(pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment)) +pub fn assert_wots_message(assert_witness: &TxAssertWitness) -> Result<[u8; 96]> { + let recover = assert_witness.recover_pi1_xd_without_verify(); + if recover.is_none() { + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + } + let (pi1, x_d) = recover.unwrap(); + Ok(pi1_xd_to_wots96_msg(&pi1, x_d)) } /// Builds a placeholder verifier challenge witness from an assert witness. pub fn build_challenge_assert_witness( verifier_state: &BabeVerifierState, - assert_witness: &BabeAssertWitness, + assert_witness: &TxAssertWitness, verifier_index: usize, ) -> Result { if verifier_state.finalized_indices.len() != BABE_M_CC { bail!("verifier state must contain exactly {BABE_M_CC} finalized BABE instances"); } - if assert_witness.pi1.is_empty() || assert_witness.wots_sig.is_empty() { - bail!("invalid assert witness"); - } + let bytes = assert_wots_message(assert_witness)?; Ok(BabeChallengeAssertWitness { verifier_index, - input_labels: (0usize..INPUT_WIRE_NUM) - .map(|index| hash16_with_index(&assert_witness.pi1, index)) - .collect(), - wots_sig: assert_witness.wots_sig.clone(), + witness: ChallengeAssertWitnessRaw { + input_labels: (0usize..INPUT_WIRE_NUM) + .map(|index| hash16_with_index(&bytes, index)) + .collect(), + wots_sig: assert_witness.wots_sig.clone(), + } }) } @@ -523,110 +420,105 @@ pub fn build_real_challenge_assert_witness( package: &CACSetupPackage, finalized_indices: &[usize], vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, operator_wots_pubkey: &OperatorAssertPublicKey, - assert_witness: &BabeAssertWitness, + assert_witness: &TxAssertWitness, verifier_index: usize, ) -> Result { if finalized_indices.len() != BABE_M_CC { bail!("verifier state must contain exactly {BABE_M_CC} finalized BABE instances"); } - let verifier = restore_real_verifier(private_state, vk, public_inputs)?; - if from_real_package(&verifier.commit()) != *package { + let verifier = restore_real_verifier(private_state, package, vk, static_inputs)?; + if verifier.commit() != *package { bail!("persisted BABE verifier state does not reproduce setup package"); } - let pi1 = ark_bn254::G1Affine::deserialize_compressed(assert_witness.pi1.as_slice()) - .map_err(|error| anyhow::anyhow!("invalid BABE pi1 in assert witness: {error}"))?; + + let recover = assert_witness.recover_pi1_xd_without_verify(); + if recover.is_none() { + return Err(anyhow::anyhow!("Cannot recover pi1 and xd")) + } + let (pi1, x_d) = recover.unwrap(); + let expected_message = pi1_xd_to_wots96_msg(&pi1, x_d); let wots_sig = to_real_wots_sig(&assert_witness.wots_sig)?; let signed_message = Wots96::signature_to_message(&wots_sig); - let expected_message = pi1_to_wots96_msg(&pi1, &assert_witness.pubin_commitment); if signed_message != expected_message { bail!("operator BABE assertion WOTS signature message does not match pi1/pubin"); } let base_idx = finalized_indices[0]; - let base_inst = verifier - .instances - .get(base_idx) - .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; - let mut input_labels = base_inst - .compute_pi1_labels_based_on_value(pi1) - .into_iter() - .skip(2) - .map(|label| label.0) - .collect::>(); - let commit = package - .commits - .get(base_idx) - .ok_or_else(|| anyhow::anyhow!("finalized base index {base_idx} out of range"))?; - input_labels.extend(pubin_input_labels(commit, &assert_witness.pubin_commitment)); - if input_labels.len() != INPUT_WIRE_NUM { - bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", input_labels.len()); + + let witness = build_challenge_assert_witness_raw( + &verifier, + assert_witness, + operator_wots_pubkey, + base_idx, + ); + if witness.is_none() { + bail!("cannot generate challenge assert witness"); } - let _ = operator_wots_pubkey; - Ok(BabeChallengeAssertWitness { verifier_index, input_labels, wots_sig: wots_sig.to_vec() }) + let witness = witness.unwrap(); + + if witness.input_labels.len() != INPUT_WIRE_NUM { + bail!("real BABE challenge labels have {}; expected {INPUT_WIRE_NUM}", witness.input_labels.len()); + } + Ok(BabeChallengeAssertWitness { + verifier_index, + witness, + }) } -/// Builds a wrongly-challenged witness from every recovered finalized-message preimage. +/// Builds a wrongly-challenged witness from a valid recovered finalized-message preimage. pub fn build_wrongly_challenged_witness( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, - final_msgs: Vec>, + final_msg: Vec, ) -> Result { build_wrongly_challenged_witness_from_preimages( &prover_state.h_msgs, challenge_witness, - final_msgs, + final_msg, ) } -/// Evaluates the native BABE garbled circuit and returns all finalized hashlock preimages. +/// Evaluates the native BABE garbled circuit and returns a finalized hashlock preimages. pub fn recover_real_wrongly_challenged_witness( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, proof: &ark_groth16::Proof, + vk: Groth16VerifyingKey, + dyn_pubin: ark_bn254::Fr, ) -> Result { - let real_state = ProverSetupState { - wots_sk_p: vec![], - finalized: prover_state - .finalized - .iter() - .map(to_real_finalized) - .collect::>>()?, - soldering: to_real_soldering(&prover_state.soldering)?, - h_msgs: prover_state.h_msgs.clone(), - presigs_v: babe_verifier_presign(), - }; - recover_all_finalized_messages(prover_state, challenge_witness, proof, &real_state) + let mut prover = BABEProver::new(vk, proof.clone(), dyn_pubin); + recover_a_valid_finalized_messages(prover_state, challenge_witness, &mut prover) } /// Builds a wrongly-challenged witness after validating all finalized preimages. pub fn build_wrongly_challenged_witness_from_preimages( h_msgs: &[[u8; 20]], challenge_witness: &BabeChallengeAssertWitness, - final_msgs: Vec>, + final_msg: Vec, ) -> Result { if h_msgs.len() != BABE_M_CC { bail!("wrongly challenged setup must contain exactly {BABE_M_CC} finalized hashlocks"); } - if final_msgs.len() != h_msgs.len() { - bail!("wrongly challenged witness must provide every finalized preimage"); - } - for (position, (final_msg, expected_hash)) in final_msgs.iter().zip(h_msgs).enumerate() { - if label_hash(final_msg) != *expected_hash { - bail!("message at finalized position {position} is not a valid preimage"); - } + + if !h_msgs.contains(&label_hash(&final_msg)) { + bail!("message is not a valid preimage"); } + Ok(BabeWronglyChallengedWitness { verifier_index: challenge_witness.verifier_index, - final_msgs, + final_msg, }) } fn ensure_real_gc_assets_configured() -> Result<()> { - for name in ["GC_GATES_PATH", "GC_INDICES_PATH"] { + for name in [ + "FGC_GATES_PATH", "FGC_OUT_INDICES_PATH", "SGC_GATES_PATH", "SGC_OUT_INDICES_PATH", + "FGC_COMPACT_GATES_PATH", "FGC_COMPACT_OUT_INDICES_PATH", "SGC_COMPACT_GATES_PATH", "SGC_COMPACT_OUT_INDICES_PATH" + ] { let path = PathBuf::from( std::env::var(name) - .map_err(|_| anyhow::anyhow!("{name} is required for real BABE setup"))?, + .map_err(|_| anyhow::anyhow!("{name} is required for real CAC setup"))?, ); if !path.is_file() { bail!("{name} does not point to a readable file: {}", path.display()); @@ -635,30 +527,27 @@ fn ensure_real_gc_assets_configured() -> Result<()> { Ok(()) } -fn statement_digest(vk: &Groth16VerifyingKey, public_inputs: &[Fr]) -> Result<[u8; 32]> { +fn statement_digest(vk: &Groth16VerifyingKey, static_inputs: Fr) -> Result<[u8; 32]> { let mut bytes = Vec::new(); vk.serialize_compressed(&mut bytes)?; - for public_input in public_inputs { - public_input.serialize_compressed(&mut bytes)?; - } + static_inputs.serialize_compressed(&mut bytes)?; Ok(hash32(&bytes)) } fn restore_real_verifier( state: &BabeVerifierPrivateState, + package: &CACSetupPackage, vk: &Groth16VerifyingKey, - public_inputs: &[Fr], + static_inputs: Fr, ) -> Result { - let instances = state - .instance_seeds - .par_iter() - .map(|seed| { - let mut instance = BABEInstance::new_from_seed(*seed); - instance.enc_setup(vk, public_inputs).map_err(anyhow::Error::msg)?; - Ok(instance) - }) - .collect::>>()?; - Ok(BABEVerifier { instances, temp_val: state.temp_val }) + let verifier = BABEVerifier::from_state( + &state.instance_seeds, package, vk, static_inputs + ); + if verifier.is_none() { + Err(anyhow::anyhow!("Cannot restore real verifier")) + } else { + Ok(verifier.unwrap()) + } } fn validate_finalized_indices( @@ -678,154 +567,6 @@ fn validate_finalized_indices( Ok(()) } -fn from_real_package(package: &RealCACSetupPackage) -> CACSetupPackage { - CACSetupPackage { commits: package.commits.iter().map(from_real_commit).collect() } -} - -fn from_real_commit(commit: &RealCACInstanceCommit) -> CACInstanceCommit { - CACInstanceCommit { - epk: commit.epk.clone(), - wots_padding_epk: padding_wire_hashes(), - constant_commits: commit.constant_commits, - h_msg: commit.h_msg, - h_ct_setup: commit.h_ct_setup, - com_adaptor: commit.com_adaptor, - com_gc: commit.com_gc, - } -} - -fn to_real_package(package: &CACSetupPackage) -> RealCACSetupPackage { - RealCACSetupPackage { - commits: package - .commits - .iter() - .map(|commit| RealCACInstanceCommit { - epk: commit.epk.clone(), - constant_commits: commit.constant_commits, - h_msg: commit.h_msg, - h_ct_setup: commit.h_ct_setup, - com_adaptor: commit.com_adaptor, - com_gc: commit.com_gc, - }) - .collect(), - } -} - -fn from_real_finalized( - finalized: &RealFinalizedInstanceData, - package: &CACSetupPackage, -) -> Result { - let real_data = RealFinalizedPayload { - gc_ciphertexts: finalized - .gc_ciphertexts - .iter() - .map(|ciphertext| ciphertext.map(|label| label.0)) - .collect(), - adaptor_table: from_real_adaptor_table(&finalized.adaptor_table), - ct_setup: SerializableSetupCt { - ct2_r_delta_g2: finalized.ct_setup.ct2_r_delta_g2.clone(), - ct3_masked_msg: finalized.ct_setup.ct3_masked_msg.clone(), - }, - constant_labels: [finalized.constant_labels[0].0, finalized.constant_labels[1].0], - wots_padding_zero_labels: [[0u8; 16]; 4], - }; - expand_compact_finalized_instance( - package, - CompactFinalizedInstanceData { index: finalized.index, real_data }, - ) -} - -fn expand_compact_finalized_instance( - package: &CACSetupPackage, - finalized: CompactFinalizedInstanceData, -) -> Result { - let commit = package - .commits - .get(finalized.index) - .ok_or_else(|| anyhow::anyhow!("finalized index {} out of range", finalized.index))?; - if commit.epk.len() != LAMPORT_N { - bail!( - "finalized index {} has {} BABE input commitments; expected {LAMPORT_N}", - finalized.index, - commit.epk.len() - ); - } - let mut wire_hashes = Vec::with_capacity(INPUT_WIRE_NUM); - wire_hashes.extend(commit.epk[..254].iter().map(to_wire_hash)); - wire_hashes.extend(commit.wots_padding_epk[..2].iter().map(to_wire_hash)); - wire_hashes.extend(commit.epk[254..].iter().map(to_wire_hash)); - wire_hashes.extend(commit.wots_padding_epk[2..].iter().map(to_wire_hash)); - wire_hashes.extend(pubin_wire_hashes(commit)); - Ok(FinalizedInstanceData { - index: finalized.index, - final_msg_hash: commit.h_msg, - wire_hashes, - gc_commitment: commit.com_gc, - adaptor_commitment: commit.com_adaptor, - ct_setup_commitment: commit.h_ct_setup, - real_data: Some(finalized.real_data), - }) -} - -fn to_real_finalized(finalized: &FinalizedInstanceData) -> Result { - let payload = finalized.real_data.as_ref().ok_or_else(|| { - anyhow::anyhow!("finalized index {} lacks real BABE payload", finalized.index) - })?; - Ok(RealFinalizedInstanceData { - index: finalized.index, - gc_ciphertexts: payload.gc_ciphertexts.iter().map(|value| value.map(S)).collect(), - adaptor_table: to_real_adaptor_table(&payload.adaptor_table)?, - ct_setup: RealSetupCt { - ct2_r_delta_g2: payload.ct_setup.ct2_r_delta_g2.clone(), - ct3_masked_msg: payload.ct_setup.ct3_masked_msg.clone(), - }, - constant_labels: [S(payload.constant_labels[0]), S(payload.constant_labels[1])], - }) -} - -fn from_real_adaptor_table(table: &RealSparseAdaptorTable) -> SerializableSparseAdaptorTable { - SerializableSparseAdaptorTable { - entries: table - .entries - .iter() - .map(|entry| SerializableSparseAdaptorEntry { - x: from_real_adaptor_row(&entry.x), - y: from_real_adaptor_row(&entry.y), - z: from_real_adaptor_row(&entry.z), - }) - .collect(), - } -} - -fn from_real_adaptor_row(row: &RealSparseAdaptorRow) -> SerializableSparseAdaptorRow { - let mut offset = Vec::new(); - row.offset.serialize_compressed(&mut offset).expect("serialize adaptor offset"); - SerializableSparseAdaptorRow { cts: row.cts.clone(), offset } -} - -fn to_real_adaptor_table(table: &SerializableSparseAdaptorTable) -> Result { - Ok(RealSparseAdaptorTable { - entries: table - .entries - .iter() - .map(|entry| { - Ok(RealSparseAdaptorEntry { - x: to_real_adaptor_row(&entry.x)?, - y: to_real_adaptor_row(&entry.y)?, - z: to_real_adaptor_row(&entry.z)?, - }) - }) - .collect::>>()?, - }) -} - -fn to_real_adaptor_row(row: &SerializableSparseAdaptorRow) -> Result { - Ok(RealSparseAdaptorRow { - cts: row.cts.clone(), - offset: Fq::deserialize_compressed(row.offset.as_slice())?, - }) -} - fn from_real_soldering(soldering: &RealSolderingData) -> Result { let output = soldering.soldering_proof.output().map_err(anyhow::Error::msg)?; Ok(SolderingData { @@ -866,131 +607,49 @@ fn to_real_soldering(soldering: &SolderingData) -> Result { }) } -fn recover_all_finalized_messages( +fn recover_a_valid_finalized_messages( prover_state: &BabeProverState, challenge_witness: &BabeChallengeAssertWitness, - proof: &ark_groth16::Proof, - real_state: &ProverSetupState, + prover: &mut BABEProver, ) -> Result { - if real_state.finalized.len() != prover_state.h_msgs.len() { + if prover_state.finalized.len() != prover_state.h_msgs.len() { bail!("BABE prover state finalized data and hash count differ"); } - if real_state.finalized.is_empty() { + if prover_state.finalized.is_empty() { bail!("BABE prover state has no finalized instances"); } - let base_input_labels = pi1_labels_from_challenge(&challenge_witness.input_labels)?; + let base_input_labels: Vec = challenge_witness.witness.input_labels.iter().map(|&b| S(b)).collect(); + // Strip the 6 dummy labels before passing to the GC (which has GC_INPUT_WIRES real wires). + let (pi1_x_labels, pi1_y_labels, x_d_labels) = deinterleave_dummy_positions(&base_input_labels); + let pi1_labels: Vec = pi1_x_labels.into_iter().chain(pi1_y_labels).collect(); + let real_soldering = to_real_soldering(&prover_state.soldering)?; + let soldered_output = &prover_state.soldering.soldered_output; - if soldered_output.base_commitment.len() != base_input_labels.len() { + if soldered_output.base_commitment.len() != GC_INPUT_WIRES { bail!( - "soldering base commitment count {} does not match BABE input label count {}", - soldered_output.base_commitment.len(), - base_input_labels.len() + "soldering base commitment count {} does not match expected GC input wire count {GC_INPUT_WIRES}", + soldered_output.base_commitment.len() ); } - let prover = BABEProver::new(proof.clone()); - let final_msgs = real_state - .finalized - .iter() - .zip(&prover_state.h_msgs) - .enumerate() - .map(|(position, (finalized, expected_hash))| { - let input_labels = - soldered_input_labels(&base_input_labels, soldered_output, position)?; - recover_finalized_message( - &prover, - proof, - finalized, - &input_labels, - *expected_hash, - position, - ) - }) - .collect::>>()?; - - Ok(BabeWronglyChallengedWitness { - verifier_index: challenge_witness.verifier_index, - final_msgs, - }) -} + let found = prover.check_compute_msg( + &prover_state.finalized, + &pi1_labels, + &x_d_labels, + &real_soldering, + &prover_state.h_msgs, + ); -fn pi1_labels_from_challenge(input_labels: &[[u8; 16]]) -> Result> { - if input_labels.len() == LAMPORT_N { - return Ok(input_labels.iter().copied().map(S).collect()); - } - if input_labels.len() != INPUT_WIRE_NUM { - bail!( - "challenge witness has {} input labels; expected {LAMPORT_N} or {INPUT_WIRE_NUM}", - input_labels.len() - ); - } - Ok(input_labels[..254].iter().chain(&input_labels[256..510]).copied().map(S).collect()) -} -fn soldered_input_labels( - base_input_labels: &[S], - soldered_output: &SolderedLabelsData, - finalized_position: usize, -) -> Result> { - if finalized_position == 0 { - return Ok(base_input_labels.to_vec()); - } - let deltas = soldered_output.deltas.get(finalized_position - 1).ok_or_else(|| { - anyhow::anyhow!("missing soldering deltas for finalized position {finalized_position}") - })?; - if deltas.len() != base_input_labels.len() { - bail!( - "soldering delta count {} does not match BABE input label count {}", - deltas.len(), - base_input_labels.len() - ); + if !found { + bail!("Cannot find any valid msg"); } - Ok(base_input_labels - .iter() - .enumerate() - .map(|(wire, &base_label)| { - let (delta_false, delta_true) = deltas[wire]; - if hash32(&base_label.0) == soldered_output.base_commitment[wire].0 { - base_label ^ S(delta_false) - } else { - base_label ^ S(delta_true) - } - }) - .collect()) -} - -fn recover_finalized_message( - prover: &BABEProver, - proof: &ark_groth16::Proof, - finalized: &RealFinalizedInstanceData, - input_labels: &[S], - expected_hash: [u8; 20], - finalized_position: usize, -) -> Result> { - let mut full_labels = Vec::with_capacity(2 + input_labels.len()); - full_labels.push(finalized.constant_labels[0]); - full_labels.push(finalized.constant_labels[1]); - full_labels.extend_from_slice(input_labels); - - let (mut circuit, gc_output_indices) = verifiable_circuit_babe::gc::read_fresh_circuit(); - let ct_prove = prover.compute_ct_prove( - &mut circuit, - &gc_output_indices, - &full_labels, - &finalized.gc_ciphertexts, - &finalized.adaptor_table, - ); - let msg = BABEProver::compute_msg(proof, &ct_prove, &finalized.ct_setup) - .map_err(anyhow::Error::msg)?; - let final_msg = msg.to_vec(); - if label_hash(&final_msg) != expected_hash { - bail!( - "recovered message at finalized position {finalized_position} does not match hashlock" - ); - } - Ok(final_msg) + Ok(BabeWronglyChallengedWitness { + verifier_index: challenge_witness.verifier_index, + final_msg: prover.valid_msg.ok_or_else(|| anyhow::anyhow!("check_compute_msg returned true but valid_msg is not set"))?.to_vec(), + }) } fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature> { @@ -1002,65 +661,10 @@ fn to_real_wots_sig(wots_sig: &[[u8; 21]]) -> Result<::Signature }) } -fn padding_wire_hashes() -> [[[u8; 20]; 2]; 4] { - let false_hash = label_hash(&vec![0u8; 16]); - let true_hash = label_hash(&vec![1u8; 16]); - [[false_hash, true_hash]; 4] -} - fn deterministic_seed(index: usize) -> u64 { u64::from_le_bytes(hash32(&(index as u64).to_le_bytes())[0..8].try_into().expect("8 bytes")) } -fn to_wire_hash(pair: &[[u8; 20]; 2]) -> WireHash { - WireHash { false_label_hash: pair[0], true_label_hash: pair[1] } -} - -fn pi1_to_wots96_msg(pi1: &ark_bn254::G1Affine, pubin_commitment: &[u8; 32]) -> [u8; 96] { - let mut msg = [0u8; 96]; - let mut tmp = Vec::new(); - - pi1.x.serialize_uncompressed(&mut tmp).expect("serialize pi1.x"); - msg[..32].copy_from_slice(&tmp); - - tmp.clear(); - pi1.y.serialize_uncompressed(&mut tmp).expect("serialize pi1.y"); - msg[32..64].copy_from_slice(&tmp); - - msg[64..96].copy_from_slice(pubin_commitment); - msg -} - -fn pubin_wire_hashes(commit: &CACInstanceCommit) -> Vec { - (0..256) - .map(|index| WireHash { - false_label_hash: label_hash(&pubin_label(commit, index, false).to_vec()), - true_label_hash: label_hash(&pubin_label(commit, index, true).to_vec()), - }) - .collect() -} - -fn pubin_input_labels(commit: &CACInstanceCommit, pubin_commitment: &[u8; 32]) -> Vec<[u8; 16]> { - (0..256) - .map(|index| { - let byte = pubin_commitment[index / 8]; - let bit = ((byte >> (index % 8)) & 1) == 1; - pubin_label(commit, index, bit) - }) - .collect() -} - -fn pubin_label(commit: &CACInstanceCommit, index: usize, bit: bool) -> [u8; 16] { - let mut bytes = Vec::with_capacity(32 * 4 + std::mem::size_of::() + 1); - bytes.extend_from_slice(&commit.h_ct_setup); - bytes.extend_from_slice(&commit.com_adaptor); - bytes.extend_from_slice(&commit.com_gc); - bytes.extend_from_slice(&commit.h_msg); - bytes.extend_from_slice(&(index as u64).to_le_bytes()); - bytes.push(u8::from(bit)); - hash16(&bytes) -} - fn hash20(data: &[u8]) -> [u8; 20] { let hash = hash32(data); hash[0..20].try_into().expect("20 bytes") diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 5895c0aaa..489d19154 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -9,8 +9,9 @@ use goat::{ scripts::{generate_opreturn_script, p2a_output}, transactions::{ assert::{pubin_disprove, validate_pubin}, - base::DUST_AMOUNT, + base::{DUST_AMOUNT, output_topology}, pre_signed::PreSignedTransaction, + watchtower_challenge::extract_operator_preimage_from_ack_txin, }, utils::num_blocks_per_network, }; @@ -179,8 +180,26 @@ pub fn validate_pubin_disprove( graph: &BitvmGcGraph, operator_commit_pubin_txin: &TxIn, operator_assert_txin: &TxIn, - ack_preimages: Vec>, + operator_ack_txins: &[TxIn], ) -> Result> { + let watchtower_num = graph.parameters.watchtower_pubkeys.len(); + let mut ack_preimages = vec![vec![]; watchtower_num]; + for txin in operator_ack_txins { + let vout = txin.previous_output.vout as usize; + if vout % 2 != 1 { + bail!("invalid ack txin in operator_ack_txins, unexpected vout: {vout}"); + } + let watchtower_index = vout / 2; + if watchtower_index >= watchtower_num + || vout != output_topology::watchtower_challenge_init::ack_connector(watchtower_index) + { + bail!("invalid ack txin in operator_ack_txins, unexpected vout: {vout}"); + } + let preimage = extract_operator_preimage_from_ack_txin(txin) + .map_err(|e| anyhow::anyhow!("failed to extract preimage from ack txin: {e}"))?; + ack_preimages[watchtower_index] = preimage; + } + let connector_e = graph.connector_e(); let connector_c = graph.connector_c(); let operator_commit_pubin_witness = connector_e diff --git a/crates/bitvm-gc/tests/babe_adapter.rs b/crates/bitvm-gc/tests/babe_adapter.rs index 19af1c2a3..c8bf07981 100644 --- a/crates/bitvm-gc/tests/babe_adapter.rs +++ b/crates/bitvm-gc/tests/babe_adapter.rs @@ -4,14 +4,12 @@ use ark_groth16::Groth16; use bitvm_gc::assert_scripts::{INPUT_WIRE_NUM, label_hash}; use bitvm_gc::babe_adapter::{ BABE_M_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, BabeVerifierState, - CACInstanceCommit, CACSetupPackage, FinalizedInstanceData, SolderingData, WOTS_SIG_COUNT, - build_assert_witness, build_challenge_assert_witness, build_real_setup_package, - build_setup_package, build_wrongly_challenged_witness, - build_wrongly_challenged_witness_from_preimages, derive_finalized_indices, - extract_gc_circuit_data, open_and_solder, open_real_setup_and_solder, verify_real_setup, - verify_setup, + CACSetupPackage, ChallengeAssertWitnessRaw, FinalizedInstanceData, SolderingData, WOTS_SIG_COUNT, + build_assert_witness, build_challenge_assert_witness, build_real_setup_package, build_setup_package, + build_wrongly_challenged_witness, build_wrongly_challenged_witness_from_preimages, derive_finalized_indices, + extract_gc_circuit_data, open_and_solder, open_real_setup_and_solder, verify_real_setup, verify_setup, + sample_cac_instance_commit, sample_finalized_instance_data }; -use bitvm_gc::operator::generate_wots_key; use rand::SeedableRng; use rand_chacha::ChaCha12Rng; use std::collections::HashSet; @@ -26,7 +24,7 @@ fn verifier_pubkey() -> bitcoin::PublicKey { } #[test] -#[ignore = "requires GC_GATES_PATH and GC_INDICES_PATH runtime assets"] +#[ignore = "requires FGC_GATES_PATH, FGC_OUT_INDICES_PATH, SGC_GATES_PATH, SGC_OUT_INDICES_PATH, FGC_COMPACT_GATES_PATH, FGC_COMPACT_OUT_INDICES_PATH, SGC_COMPACT_GATES_PATH, SGC_COMPACT_OUT_INDICES_PATH runtime assets"] fn real_setup_restores_private_state_and_verifies_soldering_proof() { let mut rng = ChaCha12Rng::seed_from_u64(42); let a = Fr::from(3_u64); @@ -36,10 +34,11 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &mut rng, ) .expect("groth16 setup"); - let public_inputs = vec![a * b]; + let static_public_inputs = a * b; + let _dynamic_public_inputs = a * a; let (package, private_state) = - build_real_setup_package(BABE_M_CC, &vk, &public_inputs).expect("real setup"); + build_real_setup_package(BABE_M_CC, &vk, static_public_inputs).expect("real setup"); let restored = serde_json::from_slice(&serde_json::to_vec(&private_state).expect("serialize state")) .expect("deserialize state"); @@ -51,7 +50,7 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &package, &finalized_indices, &vk, - &public_inputs, + static_public_inputs, ) .expect("open real setup"); @@ -64,17 +63,19 @@ fn real_setup_restores_private_state_and_verifies_soldering_proof() { &finalized, &soldering, &vk, - &public_inputs, + static_public_inputs, ) .expect("verify soldering proof"); - extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) .expect("extract native 508-wire graph data"); } #[test] fn babe_setup_payload_round_trips_and_derives_gc_data() { let package = CACSetupPackage { - commits: (0..BABE_M_CC).map(|index| CACInstanceCommit::sample(index as u8)).collect(), + commits: (0..BABE_M_CC).map(|index| sample_cac_instance_commit(index as u8)).collect(), }; let encoded = serde_json::to_vec(&package).expect("serialize package"); @@ -87,10 +88,11 @@ fn babe_setup_payload_round_trips_and_derives_gc_data() { let finalized = finalized_indices .iter() - .map(|index| FinalizedInstanceData::sample(*index)) + .map(|index| sample_finalized_instance_data(*index)) .collect::>(); - let soldering = SolderingData::sample(finalized_indices); - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let gc_data = extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) .expect("extract gc data"); assert_eq!(gc_data.verifier_pubkey, verifier_pubkey()); @@ -99,43 +101,45 @@ fn babe_setup_payload_round_trips_and_derives_gc_data() { #[test] fn protocol_finalized_instances_contribute_one_base_wire_slot() { - let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); - let soldering = SolderingData::sample((0..BABE_M_CC).collect()); + use verifiable_circuit_babe::babe::GC_INPUT_WIRES; + let package = CACSetupPackage { + commits: (0..BABE_M_CC).map(|i| sample_cac_instance_commit(i as u8)).collect(), + }; + let finalized: Vec = + (0..BABE_M_CC).map(sample_finalized_instance_data).collect(); + let h_msgs: Vec<[u8; 20]> = + finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let epk = &package.commits[finalized[0].index].epk; - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) + let gc_data = extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs) .expect("one verifier graph slot"); - assert_eq!( - gc_data.final_msg_hashlocks, - finalized.iter().map(|data| data.final_msg_hash).collect::>() - ); - assert!(gc_data.wire_hashes.as_slice() == finalized[0].wire_hashes.as_slice()); + // M finalized instances each contribute one hashlock … + assert_eq!(gc_data.final_msg_hashlocks, h_msgs); + let n = GC_INPUT_WIRES / 3; // 254 real wires per group + // pi1_x: positions 0..n + assert_eq!(gc_data.wire_hashes[0].false_label_hash, epk[0][0]); + assert_eq!(gc_data.wire_hashes[0].true_label_hash, epk[0][1]); + assert_eq!(gc_data.wire_hashes[n - 1].false_label_hash, epk[n - 1][0]); + assert_eq!(gc_data.wire_hashes[n + 2].false_label_hash, epk[n][0]); + assert_eq!(gc_data.wire_hashes[2 * (n + 2)].false_label_hash, epk[2 * n][0]); + assert_eq!(gc_data.wire_hashes[2 * (n + 2) + n - 1].false_label_hash, epk[3 * n - 1][0]); } #[test] -fn gc_slot_rejects_invalid_finalized_counts_and_soldering_order() { - for count in [1, 3, 5, 8] { - let finalized = (0..count).map(FinalizedInstanceData::sample).collect::>(); - let soldering = SolderingData::sample((0..count).collect()); - let error = match extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()) { - Ok(_) => panic!("invalid finalized count"), - Err(error) => error, - }; - assert!(error.to_string().contains(&format!("exactly {BABE_M_CC} finalized"))); +fn gc_slot_rejects_invalid_epk_length() { + use verifiable_circuit_babe::babe::GC_INPUT_WIRES; + let h_msgs = vec![[0u8; 20]; BABE_M_CC]; + for bad_len in [0, 1, GC_INPUT_WIRES - 1, GC_INPUT_WIRES + 1, INPUT_WIRE_NUM] { + let bad_epk = vec![[[0u8; 20]; 2]; bad_len]; + match extract_gc_circuit_data(verifier_pubkey(), &bad_epk, &h_msgs) { + Ok(_) => panic!("epk length {bad_len} should be rejected"), + Err(err) => assert!( + err.to_string().contains("epk has"), + "unexpected error for len {bad_len}: {err}" + ), + } } - - let finalized = (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect::>(); - let mut indices = (0..BABE_M_CC).collect::>(); - indices.swap(0, 1); - let error = match extract_gc_circuit_data( - &finalized, - &SolderingData::sample(indices), - verifier_pubkey(), - ) { - Ok(_) => panic!("mismatched soldering order"), - Err(error) => error, - }; - assert!(error.to_string().contains("soldering finalized indices mismatch")); } #[test] @@ -146,8 +150,6 @@ fn finalized_indices_reject_invalid_counts_and_cover_full_cut() { assert!(derive_finalized_indices(&package, 5).is_err()); let first = derive_finalized_indices(&package, 4).expect("derive full cut"); - let second = derive_finalized_indices(&package, 4).expect("derive full cut again"); - assert_eq!(first, second); assert_eq!(first.len(), 4); assert_eq!(first.iter().copied().collect::>().len(), 4); assert_eq!(first.iter().copied().collect::>(), HashSet::from([0, 1, 2, 3])); @@ -184,12 +186,24 @@ fn verify_setup_accepts_valid_opening_and_rejects_invalid_shapes() { #[test] fn witness_builders_validate_inputs_and_indices() { - let (assert_secret_key, _) = generate_wots_key("graph-scoped-key"); - let proof = ark_groth16::Proof::::default(); - let assert_witness = build_assert_witness(&proof, &assert_secret_key).expect("assert witness"); + use bitvm_gc::operator::generate_assert_wots_key; + let (assert_secret_key, _) = generate_assert_wots_key("test-graph-scoped-key"); + let proof = ark_groth16::Proof:: { + a: ark_bn254::G1Affine { + x: ark_bn254::Fq::from(1u64), + y: ark_bn254::Fq::from(2u64), + infinity: false, + }, + b: ark_bn254::G2Affine::default(), + c: ark_bn254::G1Affine::default(), + }; + let dynamic_input = Fr::from(0u64); + + let assert_witness = build_assert_witness(&proof, &assert_secret_key, dynamic_input) + .expect("assert witness"); assert_eq!(assert_witness.wots_sig.len(), WOTS_SIG_COUNT); - assert!(!assert_witness.pi1.is_empty()); - assert!(build_assert_witness(&proof, &Vec::new()).is_err()); + assert!(assert_witness.recover_pi1_xd_without_verify().is_some()); + assert!(build_assert_witness(&proof, &Vec::new(), dynamic_input).is_err()); let verifier_state = BabeVerifierState { package: build_setup_package(BABE_M_CC).expect("setup package"), @@ -199,61 +213,63 @@ fn witness_builders_validate_inputs_and_indices() { let challenge_witness = build_challenge_assert_witness(&verifier_state, &assert_witness, 12) .expect("challenge witness"); assert_eq!(challenge_witness.verifier_index, 12); - assert_eq!(challenge_witness.input_labels.len(), INPUT_WIRE_NUM); + assert_eq!(challenge_witness.witness.input_labels.len(), INPUT_WIRE_NUM); - let final_msgs = (0..BABE_M_CC) - .map(|index| format!("finalized-preimage-{index}").into_bytes()) - .collect::>(); - let h_msgs = final_msgs.iter().map(label_hash).collect::>(); + let final_msg = b"finalized-preimage-0".to_vec(); + let h_msgs: Vec<[u8; 20]> = (0..BABE_M_CC) + .map(|i| label_hash(&format!("finalized-preimage-{i}").into_bytes())) + .collect(); let prover_state = BabeProverState { package: build_setup_package(BABE_M_CC).expect("setup package"), - finalized: (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect(), + finalized: (0..BABE_M_CC).map(sample_finalized_instance_data).collect(), soldering: SolderingData::sample((0..BABE_M_CC).collect()), h_msgs, }; let wrongly_challenged = - build_wrongly_challenged_witness(&prover_state, &challenge_witness, final_msgs.clone()) + build_wrongly_challenged_witness(&prover_state, &challenge_witness, final_msg.clone()) .expect("wrongly challenged witness"); assert_eq!(wrongly_challenged.verifier_index, 12); - assert_eq!(wrongly_challenged.final_msgs, final_msgs); + assert_eq!(wrongly_challenged.final_msg, final_msg); assert!( build_wrongly_challenged_witness( &prover_state, &challenge_witness, - vec![b"missing-preimage".to_vec(); BABE_M_CC], + b"missing-preimage".to_vec(), ) .is_err() ); } #[test] -fn wrongly_challenged_witness_requires_all_finalized_preimages() { - let final_msgs = (0..BABE_M_CC) - .map(|index| format!("finalized-preimage-{index}").into_bytes()) - .collect::>(); - let h_msgs = final_msgs.iter().map(label_hash).collect::>(); - let challenge_witness = - BabeChallengeAssertWitness { verifier_index: 0, input_labels: vec![], wots_sig: vec![] }; +fn wrongly_challenged_witness_accepts_valid_preimage_and_rejects_invalid() { + let h_msgs: Vec<[u8; 20]> = (0..BABE_M_CC) + .map(|i| label_hash(&format!("finalized-preimage-{i}").into_bytes())) + .collect(); + let valid_msg = b"finalized-preimage-0".to_vec(); + let challenge_witness = BabeChallengeAssertWitness { + verifier_index: 0, + witness: ChallengeAssertWitnessRaw { input_labels: vec![], wots_sig: vec![] }, + }; let from_preimages = build_wrongly_challenged_witness_from_preimages( &h_msgs, &challenge_witness, - final_msgs.clone(), + valid_msg.clone(), ) .expect("wrongly challenged witness"); assert_eq!(from_preimages.verifier_index, 0); - assert_eq!(from_preimages.final_msgs, final_msgs); + assert_eq!(from_preimages.final_msg, valid_msg); let prover_state = BabeProverState { package: build_setup_package(BABE_M_CC).expect("setup package"), - finalized: (0..BABE_M_CC).map(FinalizedInstanceData::sample).collect(), + finalized: (0..BABE_M_CC).map(sample_finalized_instance_data).collect(), soldering: SolderingData::sample((0..BABE_M_CC).collect()), h_msgs, }; let delegated = build_wrongly_challenged_witness( &prover_state, &challenge_witness, - from_preimages.final_msgs.clone(), + from_preimages.final_msg.clone(), ) .expect("delegated wrongly challenged witness"); assert_eq!(delegated, from_preimages); @@ -262,8 +278,79 @@ fn wrongly_challenged_witness_requires_all_finalized_preimages() { build_wrongly_challenged_witness_from_preimages( &prover_state.h_msgs, &challenge_witness, - vec![from_preimages.final_msgs[0].clone(); BABE_M_CC], + b"not-a-valid-preimage".to_vec(), ) .is_err() ); } + +#[test] +fn setup_package_and_open_reject_degenerate_inputs() { + assert!(build_setup_package(0).is_err()); + + let pkg = build_setup_package(4).expect("setup package"); + assert!(open_and_solder(&pkg, &[0, 0]).is_err()); // duplicate finalized index + assert!(open_and_solder(&pkg, &[99]).is_err()); // out-of-range finalized index +} + +#[test] +fn witness_builders_reject_wrong_finalized_count() { + use bitvm_gc::babe_adapter::TxAssertWitness; + + // build_challenge_assert_witness rejects finalized_indices.len() != BABE_M_CC + let bad_state = BabeVerifierState { + package: build_setup_package(BABE_M_CC).expect("setup package"), + finalized_indices: vec![0], // 1, not BABE_M_CC + verifier_pubkey: verifier_pubkey(), + }; + let dummy_assert = TxAssertWitness { wots_sig: vec![] }; + assert!(build_challenge_assert_witness(&bad_state, &dummy_assert, 0).is_err()); + + // build_wrongly_challenged_witness_from_preimages rejects h_msgs.len() != BABE_M_CC + let challenge = BabeChallengeAssertWitness { + verifier_index: 0, + witness: ChallengeAssertWitnessRaw { input_labels: vec![], wots_sig: vec![] }, + }; + assert!(build_wrongly_challenged_witness_from_preimages( + &[], + &challenge, + b"msg".to_vec(), + ).is_err()); + let too_many = vec![[0u8; 20]; BABE_M_CC + 1]; + assert!(build_wrongly_challenged_witness_from_preimages( + &too_many, + &challenge, + b"msg".to_vec(), + ).is_err()); +} + +#[test] +fn assert_witness_preserves_pi1_and_dynamic_input() { + use bitvm_gc::babe_adapter::assert_wots_message; + use bitvm_gc::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("round-trip-test"); + let dynamic_input = Fr::from(42u64); + let proof = ark_groth16::Proof:: { + a: ark_bn254::G1Affine { + x: ark_bn254::Fq::from(1u64), + y: ark_bn254::Fq::from(2u64), + infinity: false, + }, + b: ark_bn254::G2Affine::default(), + c: ark_bn254::G1Affine::default(), + }; + + let assert_witness = build_assert_witness(&proof, &sk, dynamic_input).expect("assert witness"); + + let (recovered_pi1, recovered_xd) = assert_witness + .recover_pi1_xd_without_verify() + .expect("recover pi1 and xd"); + assert_eq!(recovered_pi1, proof.a); + assert_eq!(recovered_xd, dynamic_input); + + // assert_wots_message must be deterministic + let msg = assert_wots_message(&assert_witness).expect("wots message"); + let msg2 = assert_wots_message(&assert_witness).expect("wots message 2"); + assert_eq!(msg, msg2); +} diff --git a/node/src/action.rs b/node/src/action.rs index 08e22cbf6..16b87a3d1 100644 --- a/node/src/action.rs +++ b/node/src/action.rs @@ -12,7 +12,7 @@ use anyhow::{Context, Result, anyhow}; use bitcoin::{PublicKey, Txid}; use bitvm_lib::actors::Actor; use bitvm_lib::babe_adapter::{ - BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, + BabeBundleBuilder, BabeChallengeAssertWitness, BabeWronglyChallengedWitness, TxAssertWitness, CACSetupPackage, }; use bitvm_lib::committee::*; @@ -245,7 +245,7 @@ pub struct AssertSent { pub instance_id: Uuid, pub graph_id: Uuid, pub assert_txid: Txid, - pub assert_witness: Option, + pub assert_witness: Option, } #[derive(Serialize, Deserialize, Clone)] pub struct ChallengeAssertSent { diff --git a/node/src/env.rs b/node/src/env.rs index b923552d4..d79a46afe 100644 --- a/node/src/env.rs +++ b/node/src/env.rs @@ -79,12 +79,19 @@ pub const ENV_GOAT_NETWORK: &str = "GOAT_NETWORK"; pub const ENV_WATCHTOWER_PROOF_WAIT_SECS: &str = "WATCHTOWER_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_PROOF_WAIT_SECS: &str = "OPERATOR_PROOF_WAIT_SECS"; pub const ENV_OPERATOR_VK_HASH: &str = "OPERATOR_VK_HASH"; +pub const ENV_OPERATOR_ZKM_VERSION: &str = "OPERATOR_ZKM_VERSION"; pub const ENV_OPERATOR_WRAPPER_VK_HASH: &str = "OPERATOR_WRAPPER_VK_HASH"; pub const ENV_OPERATOR_WRAPPER_ZKM_VERSION: &str = "OPERATOR_WRAPPER_ZKM_VERSION"; pub const DEFAULT_WATCHTOWER_PROOF_WAIT_SECS: usize = 60; pub const DEFAULT_OPERATOR_PROOF_WAIT_SECS: usize = 60; -pub const ENV_GC_GATES_PATH: &str = "GC_GATES_PATH"; -pub const ENV_GC_INDICES_PATH: &str = "GC_INDICES_PATH"; +pub const ENV_FGC_GATES_PATH: &str = "FGC_GATES_PATH"; +pub const ENV_FGC_OUT_INDICES_PATH: &str = "FGC_OUT_INDICES_PATH"; +pub const ENV_SGC_GATES_PATH: &str = "SGC_GATES_PATH"; +pub const ENV_SGC_OUT_INDICES_PATH: &str = "SGC_OUT_INDICES_PATH"; +pub const ENV_FGC_COMPACT_GATES_PATH: &str = "FGC_COMPACT_GATES_PATH"; +pub const ENV_FGC_COMPACT_OUT_INDICES_PATH: &str = "FGC_COMPACT_OUT_INDICES_PATH"; +pub const ENV_SGC_COMPACT_GATES_PATH: &str = "SGC_COMPACT_GATES_PATH"; +pub const ENV_SGC_COMPACT_OUT_INDICES_PATH: &str = "SGC_COMPACT_OUT_INDICES_PATH"; pub const ENV_BABE_SETUP_STATE_DIR: &str = "BABE_SETUP_STATE_DIR"; pub const ENV_SOLDERING_PROOF_PAYLOAD_STORE_PATH: &str = "SOLDERING_PROOF_PAYLOAD_STORE_PATH"; @@ -601,6 +608,11 @@ pub fn get_operator_vk_hash() -> anyhow::Result<[u8; 32]> { hex_parse::<32>(&value).map_err(|err| anyhow::anyhow!("invalid {ENV_OPERATOR_VK_HASH}: {err}")) } +pub fn get_operator_zkm_version() -> anyhow::Result { + std::env::var(ENV_OPERATOR_ZKM_VERSION) + .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_ZKM_VERSION} needs to be set")) +} + pub fn get_operator_wrapper_vk_hash() -> anyhow::Result { std::env::var(ENV_OPERATOR_WRAPPER_VK_HASH) .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_VK_HASH} needs to be set")) @@ -611,23 +623,37 @@ pub fn get_operator_wrapper_zkm_version() -> anyhow::Result { .map_err(|_| anyhow::anyhow!("{ENV_OPERATOR_WRAPPER_ZKM_VERSION} needs to be set")) } -/// Returns the configured GC asset paths after checking that they are readable files. -/// TODO: maybe multi files -pub fn get_babe_gc_asset_paths() -> anyhow::Result<(PathBuf, PathBuf)> { - let gates_path = PathBuf::from( - std::env::var(ENV_GC_GATES_PATH) - .map_err(|_| anyhow::anyhow!("{ENV_GC_GATES_PATH} is missing"))?, - ); - let indices_path = PathBuf::from( - std::env::var(ENV_GC_INDICES_PATH) - .map_err(|_| anyhow::anyhow!("{ENV_GC_INDICES_PATH} is missing"))?, - ); - for (name, path) in [(ENV_GC_GATES_PATH, &gates_path), (ENV_GC_INDICES_PATH, &indices_path)] { +pub struct BabeGcAssetPaths { + pub fgc_gates: PathBuf, + pub fgc_out_indices: PathBuf, + pub sgc_gates: PathBuf, + pub sgc_out_indices: PathBuf, + pub fgc_compact_gates: PathBuf, + pub fgc_compact_out_indices: PathBuf, + pub sgc_compact_gates: PathBuf, + pub sgc_compact_out_indices: PathBuf, +} + +pub fn get_babe_gc_asset_paths() -> anyhow::Result { + fn read_path(name: &str) -> anyhow::Result { + let path = PathBuf::from( + std::env::var(name).map_err(|_| anyhow::anyhow!("{name} is missing"))?, + ); if !path.is_file() { anyhow::bail!("{name} does not point to a readable file: {}", path.display()); } + Ok(path) } - Ok((gates_path, indices_path)) + Ok(BabeGcAssetPaths { + fgc_gates: read_path(ENV_FGC_GATES_PATH)?, + fgc_out_indices: read_path(ENV_FGC_OUT_INDICES_PATH)?, + sgc_gates: read_path(ENV_SGC_GATES_PATH)?, + sgc_out_indices: read_path(ENV_SGC_OUT_INDICES_PATH)?, + fgc_compact_gates: read_path(ENV_FGC_COMPACT_GATES_PATH)?, + fgc_compact_out_indices: read_path(ENV_FGC_COMPACT_OUT_INDICES_PATH)?, + sgc_compact_gates: read_path(ENV_SGC_COMPACT_GATES_PATH)?, + sgc_compact_out_indices: read_path(ENV_SGC_COMPACT_OUT_INDICES_PATH)?, + }) } pub fn get_instance_maintenance_batch_size() -> u32 { diff --git a/node/src/handle.rs b/node/src/handle.rs index eb2f385cd..e67785c81 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -15,13 +15,7 @@ use ark_serialize::{CanonicalDeserialize, CanonicalSerialize}; use bitcoin::{Amount, OutPoint, Txid}; use bitcoin::{PublicKey, XOnlyPublicKey}; use bitvm_lib::actors::Actor; -use bitvm_lib::babe_adapter::{ - BABE_M_CC, BABE_N_CC, BabeAssertWitness, BabeBundleBuilder, BabeChallengeAssertWitness, - BabeProverState, CACSetupPackage, CompactSolderingProofPayload, assert_wots_message, - build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, - derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, - open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, -}; +use bitvm_lib::babe_adapter::{BABE_M_CC, BABE_N_CC, BabeBundleBuilder, BabeChallengeAssertWitness, BabeProverState, ChallengeAssertWitnessRaw, CompactSolderingProofPayload, TxAssertWitness, assert_wots_message, build_assert_witness, build_real_challenge_assert_witness, build_real_setup_package, derive_finalized_indices, expand_compact_soldering_proof_payload, extract_gc_circuit_data, open_real_setup_and_solder, recover_real_wrongly_challenged_witness, verify_real_setup, CACSetupPackage, WOTS_SIG_COUNT}; use bitvm_lib::committee::*; use bitvm_lib::keys::*; use bitvm_lib::operator::*; @@ -34,10 +28,11 @@ use goat::connectors::connector_z::ConnectorZ; use goat::transactions::base::output_topology; use goat::transactions::pre_signed::PreSignedTransaction; use goat::transactions::pre_signed_musig2::verify_public_nonce; +use goat::wots::{Wots, Wots96}; use libp2p::gossipsub::MessageId; use libp2p::{PeerId, Swarm}; use std::sync::Arc; -use store::GraphStatus; +use store::{GraphStatus, SerializableTxid}; use store::localdb::LocalDB; use uuid::Uuid; @@ -1208,9 +1203,9 @@ async fn handle_init_graph_verifier( } else { get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE setup")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let (setup_package, private_state) = tokio::task::spawn_blocking(move || { - build_real_setup_package(BABE_N_CC, &vk, &public_inputs) + build_real_setup_package(BABE_N_CC, &vk, static_input) }) .await .context("real BABE setup task failed")??; @@ -1407,7 +1402,7 @@ async fn handle_cut_circuits_verifier( get_babe_gc_asset_paths()?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE opening")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let private_state = verifier_state.private_state.clone(); let selected_indices = selected_circuit_indexes.clone(); let package_for_opening = setup_package.clone(); @@ -1423,7 +1418,7 @@ async fn handle_cut_circuits_verifier( &package_for_opening, &selected_indices, &vk, - &public_inputs, + static_input, ) }) .await @@ -1659,11 +1654,11 @@ async fn handle_compact_soldering_proof_operator( } let setup_package = candidate.setup_package.clone(); let (opened, finalized, soldering) = - expand_compact_soldering_proof_payload(&setup_package, payload) + expand_compact_soldering_proof_payload(payload) .context("expand compact soldering proof payload")?; let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE validation")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let package_for_validation = setup_package.clone(); let opened_for_validation = opened.clone(); let finalized_for_validation = finalized.clone(); @@ -1682,13 +1677,18 @@ async fn handle_compact_soldering_proof_operator( &finalized_for_validation, &soldering_for_validation, &vk, - &public_inputs, + static_input, ) }) .await .context("real BABE setup verification task failed")??; - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey)?; + if finalized.len() != BABE_M_CC { + bail!("each verifier must contribute exactly {BABE_M_CC} finalized BABE instances"); + } + let epk = &setup_package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| setup_package.commits[f.index].h_msg).collect(); + let gc_data = extract_gc_circuit_data(verifier_pubkey, epk, &h_msgs)?; let prover_state = BabeProverState { package: setup_package.clone(), finalized, @@ -3560,17 +3560,52 @@ async fn handle_operator_commit_pubin_ready_operator( content: &GOATMessageContent, ) -> Result<()> { let message = make_message(ctx, content); - let Some((_graph, graph_status, _graph_sub_status)) = + let Some((graph, _graph_status, _graph_sub_status)) = refresh_graph_status(ctx, instance_id, graph_id, Some(&message), GraphStatus::Challenge) .await? else { return Ok(()); }; - // TODO(gc-v2): build operator-commit-pubin after pubin commitment data is wired. - // It is not pre-signed, so add a funding input for fees and broadcast it normally. - tracing::warn!( - "OperatorCommitPubinReady for {instance_id}:{graph_id} is not implemented yet; graph status is {graph_status:?}" + + // compute the 96-byte guest pubin + let watchtower_challenge_init_txid = + SerializableTxid::from(graph.watchtower_challenge_init.tx().compute_txid()); + let num_watchtowers = graph.parameters.watchtower_pubkeys.len(); + let (challenge_txids, included_watchtowers_bits) = + get_watchtower_challenge_info(ctx.btc_client, &watchtower_challenge_init_txid, num_watchtowers) + .await?; + let (btc_best_block_hash, included_watchtowers_bitmap) = + compute_operator_pubin_blockhash_and_bitmap( + ctx.btc_client, + &challenge_txids, + &included_watchtowers_bits, + ) + .await?; + let guest_pubin = build_operator_guest_pubin( + &btc_best_block_hash, + &graph.parameters.pubin_disprove_constant, + &included_watchtowers_bitmap, ); + + // sign and broadcast + let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); + let commit_pubin_wots_sk = operator_master_key.commit_pubin_wots_keypair_for_graph(graph_id).0; + let signed_input = operator_sign_commit_pubin(&graph, &commit_pubin_wots_sk, &guest_pubin)?; + let connector_e_amount = graph + .watchtower_challenge_init + .connector_e_input() + .map_err(|e| anyhow!("failed to get connector-e input: {e}"))? + .amount; + let operator_keypair = operator_master_key.master_keypair(); + build_sign_and_broadcast_tx( + ctx.btc_client, + operator_keypair, + vec![signed_input], + connector_e_amount, + vec![], + ) + .await?; + Ok(()) } @@ -3634,6 +3669,7 @@ async fn handle_operator_commit_pubin_timeout_verifier( Ok(()) } + // after the watchtower challenge flow is complete, build proof and broadcast Assert transaction. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_assert_ready_operator( @@ -3651,7 +3687,7 @@ async fn handle_assert_ready_operator( Some(v) => v, None => return Ok(()), }; - let (operator_wrapper_proof, wait_secs) = get_operator_wrapper_proof( + let (operator_proof, wait_secs) = get_operator_proof( ctx.local_db, ctx.http_client, &graph, @@ -3669,15 +3705,23 @@ async fn handle_assert_ready_operator( return Ok(()); } - let Some(operator_wrapper_proof) = operator_wrapper_proof else { + let Some(operator_proof) = operator_proof else { return Ok(()); }; let operator_master_key = OperatorMasterKey::new(get_bitvm_key()?); let assert_secret_key = operator_master_key.assert_wots_keypair_for_graph(graph_id).0; - let assert_witness = build_assert_witness(&operator_wrapper_proof.proof, &assert_secret_key)?; + + if operator_proof.public_inputs.len() != 2 { + bail!( + "operator proof has {} public inputs; expected 2", + operator_proof.public_inputs.len() + ); + } + let dynamic_input = operator_proof.public_inputs[1]; + let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; let mut asserted_wrapper_proof = Vec::new(); - operator_wrapper_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; + operator_proof.proof.serialize_compressed(&mut asserted_wrapper_proof)?; let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .ok_or_else(|| anyhow!("missing operator BABE setup state for graph {graph_id}"))?; let operator_state = setup_state @@ -3706,6 +3750,61 @@ async fn handle_assert_ready_operator( Ok(()) } +/// Recovers a `BabeChallengeAssertWitness` from the on-chain challenge assert tx. +fn recover_challenge_assert_witness( + challenge_assert_tx: &bitcoin::Transaction, + verifier_index: usize, +) -> Result { + let txin = challenge_assert_tx + .input + .first() + .ok_or_else(|| anyhow!("challenge assert tx has no input"))?; + let items: Vec> = txin.witness.to_vec(); + let expected_len = 2 * WOTS_SIG_COUNT + goat::assert_scripts::INPUT_WIRE_NUM + 2; + if items.len() != expected_len { + bail!( + "unexpected challenge assert witness length: {} (expected {expected_len})", + items.len() + ); + } + let mut bitcoin_witness = bitcoin::Witness::new(); + for item in &items[..2 * WOTS_SIG_COUNT] { + bitcoin_witness.push(item); + } + let wots_sig = Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec(); + let input_labels: Vec<[u8; 16]> = items + [2 * WOTS_SIG_COUNT..2 * WOTS_SIG_COUNT + goat::assert_scripts::INPUT_WIRE_NUM] + .iter() + .map(|item| item.as_slice().try_into()) + .collect::>() + .map_err(|_| anyhow!("challenge assert label item is not 16 bytes"))?; + Ok(BabeChallengeAssertWitness { + verifier_index, + witness: ChallengeAssertWitnessRaw { input_labels, wots_sig }, + }) +} + +/// Collects the on-chain ACK TxIns from all watchtowers that submitted a challenge_ack. +/// Only connectors that are already spent are included; unspent ones are skipped. +async fn collect_ack_txins( + btc_client: &BTCClient, + wci_txid: &Txid, + num_watchtowers: usize, +) -> Result> { + let mut ack_txins = Vec::new(); + for i in 0..num_watchtowers { + let ack_vout = output_topology::watchtower_challenge_init::ack_connector(i) as u64; + if let Some(ack_txid) = outpoint_spent_txid(btc_client, wci_txid, ack_vout).await? { + if let Some(ack_tx) = btc_client.get_tx(&ack_txid).await? { + if let Some(txin) = ack_tx.input.first().cloned() { + ack_txins.push(txin); + } + } + } + } + Ok(ack_txins) +} + // verify Operator DynamicPublicInput and Proof; broadcast PubinDisprove or ChallengeAssert as needed. #[tracing::instrument(level = "info", skip_all, fields(instance_id = %instance_id, graph_id = %graph_id))] async fn handle_assert_sent_verifier( @@ -3713,9 +3812,8 @@ async fn handle_assert_sent_verifier( instance_id: Uuid, graph_id: Uuid, assert_txid: Txid, - assert_witness: &Option, + assert_witness: &Option, ) -> Result<()> { - // TODO: check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction let (graph, _graph_status, _graph_sub_status) = match refresh_graph_status(ctx, instance_id, graph_id, None, GraphStatus::Challenge).await? { @@ -3729,13 +3827,6 @@ async fn handle_assert_sent_verifier( ); return Ok(()); }; - let Some(assert_witness) = assert_witness else { - // TODO(gc-v2): recover BabeAssertWitness from the assert tx witness and graph. - tracing::warn!( - "Ignore AssertSent for {instance_id}:{graph_id}: assert witness is not available in message" - ); - return Ok(()); - }; let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); let verifier_pubkey = verifier_master_key.master_keypair().public_key().into(); @@ -3749,6 +3840,86 @@ async fn handle_assert_sent_verifier( }; validate_verifier_slot(&graph, verifier_index)?; + // check pubin first, if invalid, directly send PubinDisprove without building ChallengeAssert transaction + if let Ok(connector_e_input) = graph.watchtower_challenge_init.connector_e_input() { + let outpoint = connector_e_input.outpoint; + if let Some(commit_pubin_txid) = + outpoint_spent_txid(ctx.btc_client, &outpoint.txid, outpoint.vout as u64).await? + { + if let Some(commit_pubin_tx) = ctx.btc_client.get_tx(&commit_pubin_txid).await? { + if let Some(commit_pubin_txin) = commit_pubin_tx.input.first() { + let assert_txin = assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?; + let wci_txid = graph.watchtower_challenge_init.tx().compute_txid(); + let num_watchtowers = graph.parameters.watchtower_ack_hashlocks.len(); + let ack_txins = collect_ack_txins(ctx.btc_client, &wci_txid, num_watchtowers).await?; + match validate_pubin_disprove(&graph, commit_pubin_txin, assert_txin, &ack_txins) { + Ok(Some((witness_data, _))) => { + // build pubin disprove Tx + let pubin_disprove_txin = + build_pubin_disprove_txin(&graph, witness_data)?; + let pubin_disprove_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![pubin_disprove_txin], + output: vec![goat::scripts::p2a_output()], + }; + broadcast_nonstandard_tx(ctx.btc_client, &pubin_disprove_tx).await?; + let message_content = + GOATMessageContent::DisproveSent(DisproveSent { + instance_id, + graph_id, + disprove_type: DisproveTxType::PubinDisprove, + index: verifier_index, + challenge_start_txid: None, + challenge_finish_txid: pubin_disprove_tx.compute_txid(), + }); + send_to_peer( + ctx.swarm, + GOATMessage::new(Actor::Committee, message_content), + ) + .await?; + return Ok(()); + } + Ok(None) => tracing::debug!( + "PubinDisprove invalid for {instance_id}:{graph_id}: operator pubin consistent, proceeding to ChallengeAssert" + ), + Err(e) => tracing::warn!( + "PubinDisprove check failed for {instance_id}:{graph_id}: {e}, proceeding to ChallengeAssert" + ), + } + } + } + } + } + + // recover TxAssertWitness from the assert tx witness and graph. + let assert_witness_recovered: TxAssertWitness; + let assert_witness: &TxAssertWitness = match assert_witness { + Some(w) => w, + None => { + let raw_witness = graph + .connector_c() + .extract_leaf_1_raw_witness( + assert_tx + .input + .first() + .ok_or_else(|| anyhow!("operator assert transaction has no input"))?, + ) + .map_err(|e| anyhow!("failed to extract WOTS signature from assert tx: {e}"))?; + let mut bitcoin_witness = bitcoin::Witness::new(); + for item in &raw_witness { + bitcoin_witness.push(item); + } + assert_witness_recovered = TxAssertWitness { + wots_sig: Wots96::raw_witness_to_signature(&bitcoin_witness).to_vec(), + }; + &assert_witness_recovered + } + }; + let Some(saved_verifier_state) = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? .and_then(|state| state.verifier) else { @@ -3764,18 +3935,19 @@ async fn handle_assert_sent_verifier( return Ok(()); } let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE challenge")?; - let public_inputs = derive_operator_wrapper_statement(graph_id)?.public_inputs; + let static_input = derive_operator_statement(graph_id)?.static_input; let challenge_witness = build_real_challenge_assert_witness( &saved_verifier_state.private_state, &saved_verifier_state.setup_package, &saved_verifier_state.finalized_indices, &vk, - &public_inputs, + static_input, &graph.parameters.operator_assert_wots_pubkey, assert_witness, verifier_index, )?; let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .witness .input_labels .iter() .map(|label| label.to_vec()) @@ -3825,16 +3997,7 @@ async fn handle_challenge_assert_sent_operator( Some(v) => v, None => return Ok(()), }; - let Some(challenge_witness) = challenge_witness else { - // TODO(gc-v2): recover BabeChallengeAssertWitness from the challenge tx witness and graph. - tracing::warn!( - "Ignore ChallengeAssertSent for {instance_id}:{graph_id}: challenge witness is not available in message" - ); - return Ok(()); - }; - validate_verifier_slot(&graph, verifier_index)?; - validate_challenge_witness_index(verifier_index, challenge_witness)?; validate_expected_challenge_assert_txid(&graph, verifier_index, challenge_assert_txid)?; let Some(challenge_assert_tx) = ctx.btc_client.get_tx(&challenge_assert_txid).await? else { @@ -3848,7 +4011,19 @@ async fn handle_challenge_assert_sent_operator( return Ok(()); }; + let challenge_witness_recovered: BabeChallengeAssertWitness; + let challenge_witness: &BabeChallengeAssertWitness = match challenge_witness { + Some(w) => w, + None => { + challenge_witness_recovered = + recover_challenge_assert_witness(&challenge_assert_tx, verifier_index)?; + &challenge_witness_recovered + } + }; + validate_challenge_witness_index(verifier_index, challenge_witness)?; + let labels: [Vec; goat::assert_scripts::INPUT_WIRE_NUM] = challenge_witness + .witness .input_labels .iter() .map(|label| label.to_vec()) @@ -3904,15 +4079,18 @@ async fn handle_challenge_assert_sent_operator( .ok_or_else(|| anyhow!("missing asserted wrapper proof for graph {graph_id}"))?; let proof = Groth16Proof::deserialize_compressed(proof_bytes.as_slice()) .context("deserialize asserted wrapper proof")?; + let vk = crate::vk::get_vk().await.context("load Groth16 verifying key for BABE wrongly challenged")?; + let (_, dyn_pubin) = TxAssertWitness { wots_sig: challenge_witness.witness.wots_sig.clone() } + .recover_pi1_xd_without_verify() + .ok_or_else(|| anyhow!("cannot recover dynamic input from challenge witness WOTS signature"))?; let wrongly_challenged_witness = - recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof)?; - // TODO: The wrongly-challenged script accepts any one finalized-message preimage. - let final_msg = wrongly_challenged_witness - .final_msgs - .first() - .ok_or_else(|| anyhow!("wrongly challenged witness has no final message preimage"))?; - let (wrongly_challenged_input, _amount) = - operator_sign_wrongly_challenged(&graph, verifier_index, final_msg)?; + recover_real_wrongly_challenged_witness(prover_state, challenge_witness, &proof, vk, dyn_pubin)?; + let (wrongly_challenged_input, _amount) = operator_sign_wrongly_challenged( + &graph, + verifier_index, + &wrongly_challenged_witness.final_msg, + )?; + let wrongly_challenged_tx = bitcoin::Transaction { version: bitcoin::transaction::Version(2), lock_time: bitcoin::absolute::LockTime::ZERO, @@ -4546,7 +4724,7 @@ async fn handle_response_node_info( #[cfg(test)] mod tests { use std::str::FromStr; - + use bitcoin::hashes::Hash; use bitvm_lib::babe_adapter::{ BABE_M_CC, BabeProverState, build_setup_package, open_and_solder, }; @@ -4562,7 +4740,9 @@ mod tests { let package = build_setup_package(BABE_M_CC + 1).unwrap(); let selected = (0..BABE_M_CC).collect::>(); let (_, finalized, soldering) = open_and_solder(&package, &selected).unwrap(); - let gc_data = extract_gc_circuit_data(&finalized, &soldering, verifier_pubkey()).unwrap(); + let epk = &package.commits[finalized[0].index].epk; + let h_msgs: Vec<[u8; 20]> = finalized.iter().map(|f| package.commits[f.index].h_msg).collect(); + let gc_data = extract_gc_circuit_data(verifier_pubkey(), epk, &h_msgs).unwrap(); let prover_state = BabeProverState { package: package.clone(), finalized, @@ -4665,4 +4845,144 @@ mod tests { assert!(restored.candidates[0].prover_state.is_none()); } + + // ── collect_ack_txins ───────────────────────────────────────────────────── + + fn make_txid(byte: u8) -> Txid { + Txid::from_byte_array([byte; 32]) + } + + fn make_confirmed_ack_tx(ack_txid: Txid, wci_txid: Txid, ack_vout: u32) -> esplora_client::Tx { + use esplora_client::{TxStatus, Vin}; + let preimage = b"test-ack-preimage".to_vec(); + esplora_client::Tx { + txid: ack_txid, + version: 2, + locktime: 0, + vin: vec![Vin { + txid: wci_txid, + vout: ack_vout, + prevout: None, + scriptsig: bitcoin::ScriptBuf::default(), + witness: vec![preimage], + sequence: 1, + is_coinbase: false, + }], + vout: vec![], + size: 1, + weight: 1, + status: TxStatus { + confirmed: true, + block_height: Some(800_000), + block_hash: None, + block_time: Some(1_000_000), + }, + fee: 0, + } + } + + #[tokio::test] + async fn collect_ack_txins_returns_only_spent_connectors() { + let (btc_client, mock) = client::btc_chain::BTCClient::new_mock_client(); + let wci_txid = make_txid(0x00); + let ack_txid_0 = make_txid(0x01); + let num_watchtowers = 3; + + // Watchtower 0 ACKed + let vout_0 = output_topology::watchtower_challenge_init::ack_connector(0) as u32; + mock.set_tx(ack_txid_0, make_confirmed_ack_tx(ack_txid_0, wci_txid, vout_0)); + + // Watchtower 1 did NOT ACK (connector unspent — nothing registered) + + // Watchtower 2 ACKed + let ack_txid_2 = make_txid(0x02); + let vout_2 = output_topology::watchtower_challenge_init::ack_connector(2) as u32; + mock.set_tx(ack_txid_2, make_confirmed_ack_tx(ack_txid_2, wci_txid, vout_2)); + + let txins = collect_ack_txins(&btc_client, &wci_txid, num_watchtowers).await.unwrap(); + + assert_eq!(txins.len(), 2); + assert_eq!(txins[0].previous_output.vout, vout_0); + assert_eq!(txins[1].previous_output.vout, vout_2); + } + + // ── recover_challenge_assert_witness ────────────────────────────────────── + + fn build_challenge_assert_tx( + sig: &::Signature, + labels: &[[u8; 16]], + ) -> bitcoin::Transaction { + let mut witness = bitcoin::Witness::new(); + let raw_wots = Wots96::signature_to_raw_witness(sig); + for item in raw_wots.iter() { + witness.push(item); + } + for label in labels { + witness.push(label.as_slice()); + } + witness.push(&[0xde, 0xad]); // placeholder script + witness.push(&[0xbe, 0xef]); // placeholder control block + bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![bitcoin::TxIn { witness, ..Default::default() }], + output: vec![], + } + } + + #[test] + fn recover_challenge_assert_witness_extracts_wots_sig_and_labels() { + use bitvm_lib::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("test-challenge-witness"); + let msg = [0x42u8; 96]; + let sig = Wots96::sign(&sk, &msg); + + let labels: Vec<[u8; 16]> = (0..goat::assert_scripts::INPUT_WIRE_NUM) + .map(|i| [(i % 256) as u8; 16]) + .collect(); + + let tx = build_challenge_assert_tx(&sig, &labels); + let verifier_index = 3; + let recovered = recover_challenge_assert_witness(&tx, verifier_index).unwrap(); + + assert_eq!(recovered.verifier_index, verifier_index); + assert_eq!(recovered.witness.wots_sig.len(), WOTS_SIG_COUNT); + assert_eq!(recovered.witness.input_labels.len(), goat::assert_scripts::INPUT_WIRE_NUM); + + let mut bw = bitcoin::Witness::new(); + for item in Wots96::signature_to_raw_witness(&sig).iter() { + bw.push(item); + } + assert_eq!(recovered.witness.wots_sig, Wots96::raw_witness_to_signature(&bw).to_vec()); + + // labels are correctly extracted + for (i, label) in recovered.witness.input_labels.iter().enumerate() { + assert_eq!(label[0], (i % 256) as u8, "label {i} mismatch"); + } + } + + #[test] + fn recover_challenge_assert_witness_rejects_wrong_length() { + use bitvm_lib::operator::generate_assert_wots_key; + + let (sk, _) = generate_assert_wots_key("test-wrong-len"); + let sig = Wots96::sign(&sk, &[0u8; 96]); + let labels: Vec<[u8; 16]> = vec![[0u8; 16]; goat::assert_scripts::INPUT_WIRE_NUM]; + + // one label short + let mut short_labels = labels.clone(); + short_labels.pop(); + let bad_tx = build_challenge_assert_tx(&sig, &short_labels); + assert!(recover_challenge_assert_witness(&bad_tx, 0).is_err()); + + // no inputs at all + let empty_tx = bitcoin::Transaction { + version: bitcoin::transaction::Version(2), + lock_time: bitcoin::absolute::LockTime::ZERO, + input: vec![], + output: vec![], + }; + assert!(recover_challenge_assert_witness(&empty_tx, 0).is_err()); + } } diff --git a/node/src/utils.rs b/node/src/utils.rs index 9d9f6a7bf..08c6c0f0f 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -25,7 +25,8 @@ use bitcoin::{ XOnlyPublicKey, }; use bitcoin_light_client_circuit::{ - VK_HASH_SIZE, build_watchtower_commitment, wrapper_public_values, + VK_HASH_SIZE, build_watchtower_commitment, decode_operator_public_outputs, + wrapper_public_values, }; use bitvm::treepp::*; use bitvm_lib::actors::Actor; @@ -116,6 +117,16 @@ pub type VerifyingKey = ark_groth16::VerifyingKey; pub type Groth16Proof = ark_groth16::Proof; pub type PublicInputs = Vec; +#[derive(Clone)] +pub struct ValidatedOperatorProof { + pub proof: Groth16Proof, + pub public_inputs: PublicInputs, + pub verifying_key: VerifyingKey, + pub public_values: Vec, + pub vk_hash: String, + pub zkm_version: String, +} + #[derive(Clone)] pub struct ValidatedOperatorWrapperProof { pub proof: Groth16Proof, @@ -126,6 +137,14 @@ pub struct ValidatedOperatorWrapperProof { pub zkm_version: String, } +#[derive(Clone)] +pub struct OperatorStatement { + pub static_input: ark_bn254::Fr, + pub vk_hash: [u8; 32], + pub zkm_version: String, + pub constant: [u8; 32], +} + #[derive(Clone)] pub struct OperatorWrapperStatement { pub public_values: Vec, @@ -1957,11 +1976,83 @@ pub async fn get_watchtower_challenge_info( } Ok((challenge_txids, included_watchtowers)) } + +/// Returns `(btc_best_block_hash, included_watchtowers_bitmap)` from already-fetched +/// `get_watchtower_challenge_info` output. +pub async fn compute_operator_pubin_blockhash_and_bitmap( + btc_client: &BTCClient, + challenge_txids: &[Option], + included_watchtowers_bits: &[bool], +) -> Result<([u8; 32], [u8; 32])> { + let btc_best_block_hash = { + let mut largest: Option<(u32, BlockHash)> = None; + for txid in challenge_txids.iter().flatten() { + let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; + let (height, hash) = match (status.block_height, status.block_hash) { + (Some(height), Some(hash)) => (height, hash), + _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), + }; + if largest.is_none_or(|(h, _)| height > h) { + largest = Some((height, hash)); + } + } + largest + .map(|(_, hash)| hash.to_byte_array()) + .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx available"))? + }; + + let mut included_watchtowers = [0u8; 32]; + for (i, &included) in included_watchtowers_bits.iter().enumerate() { + if included && i < 256 { + included_watchtowers[i / 8] |= 1 << (i % 8); + } + } + + Ok((btc_best_block_hash, included_watchtowers)) +} + +/// Assembles the 96-byte guest pubin: +pub fn build_operator_guest_pubin( + btc_best_block_hash: &[u8; 32], + pubin_disprove_constant: &[u8; 32], + included_watchtowers: &[u8; 32], +) -> [u8; 96] { + let mut pubin = [0u8; 96]; + pubin[0..32].copy_from_slice(btc_best_block_hash); + pubin[32..64].copy_from_slice(pubin_disprove_constant); + pubin[64..96].copy_from_slice(included_watchtowers); + pubin +} + fn load_part_stark_vk_for_zkm_version(zkm_version: &str) -> Result> { catch_unwind(AssertUnwindSafe(|| Groth16Verifier::get_part_stark_vk(zkm_version).to_vec())) .map_err(|_| anyhow!("failed to load part_stark_vk for zkm_version {zkm_version}")) } +fn combined_operator_vk_hash(operator_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { + if !operator_vk_hash.starts_with("0x") { + bail!("configured operator vk hash must use 0x-prefixed Ziren encoding"); + } + let raw_vk_hash = decode_zkm_vkey_hash(operator_vk_hash) + .map_err(|e| anyhow!("invalid configured operator vk hash: {e:?}"))?; + let part_vk: PartStarkVerifyingKey = + bincode::deserialize(&load_part_stark_vk_for_zkm_version(zkm_version)?) + .context("deserialize operator partial STARK verifying key")?; + let base = Bn254Fr::from_canonical_u32(256); + let mut field_hash = Bn254Fr::ZERO; + for byte in raw_vk_hash { + field_hash = field_hash * base + Bn254Fr::from_canonical_u32(byte as u32); + } + let combined = zkm_recursion_core::hash_vkey_with_part_vk(&part_vk, field_hash); + let bytes = combined.as_canonical_biguint().to_bytes_be(); + if bytes.len() > 32 { + bail!("combined operator verifying key hash exceeds BN254 field encoding"); + } + let mut encoded = [0u8; 32]; + encoded[32 - bytes.len()..].copy_from_slice(&bytes); + Ok(encoded) +} + fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result<[u8; 32]> { if !wrapper_vk_hash.starts_with("0x") { bail!("configured wrapper vk hash must use 0x-prefixed Ziren encoding"); @@ -1986,6 +2077,15 @@ fn combined_wrapper_vk_hash(wrapper_vk_hash: &str, zkm_version: &str) -> Result< Ok(encoded) } +pub fn derive_operator_statement(graph_id: Uuid) -> Result { + let vk_hash = get_operator_vk_hash()?; + let zkm_version = get_operator_zkm_version()?; + let combined_hash = combined_operator_vk_hash(&format!("0x{}", hex::encode(vk_hash)), &zkm_version)?; + let static_input = load_ark_public_inputs_from_bytes(&combined_hash, &[0u8; 32])[0]; + let constant = hash_operator_constant(*graph_id.as_bytes(), get_genesis_sequencer_commit_id()); + Ok(OperatorStatement { static_input, vk_hash, zkm_version, constant }) +} + pub fn derive_operator_wrapper_statement(graph_id: Uuid) -> Result { let public_values = wrapper_public_values( get_operator_vk_hash()?, @@ -2043,22 +2143,13 @@ pub async fn get_operator_wrapper_proof( return Ok((None, get_operator_proof_wait_secs())); } }; - let operator_committed_blockhash = { - let mut largest: Option<(u32, BlockHash)> = None; - for txid in watchtower_challenge_txids.iter().flatten() { - let status = btc_client.get_tx_status(&Txid::from_str(txid)?).await?; - let (height, hash) = match (status.block_height, status.block_hash) { - (Some(height), Some(hash)) => (height, hash), - _ => bail!("watchtower challenge tx {txid} is not confirmed yet"), - }; - if largest.is_none_or(|(largest_height, _)| height > largest_height) { - largest = Some((height, hash)); - } - } - largest - .map(|(_, hash)| hash.to_string()) - .ok_or_else(|| anyhow!("no confirmed watchtower challenge tx is available"))? - }; + let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( + btc_client, + &watchtower_challenge_txids, + &included_watchtowers, + ) + .await?; + let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); let base_url = Url::parse( &get_proof_build_rpc_host() @@ -2149,6 +2240,129 @@ pub async fn get_operator_wrapper_proof( 0, )) } + +/// Returns: +/// - `Ok(Some(OperatorProof), _)` if operator proof is available and valid +/// - `Ok(None, wait_secs)` if operator proof is not yet available +pub async fn get_operator_proof( + local_db: &LocalDB, + http_client: &HttpAsyncClient, + bitvm_graph: &BitvmGcGraph, + btc_client: &BTCClient, + instance_id: Uuid, + graph_id: Uuid, +) -> Result<(Option, usize)> { + let mut storage_processor = local_db.acquire().await?; + let Some(graph) = storage_processor.find_graph(&graph_id).await? else { + warn!("graph:{graph_id} not found"); + bail!("No graph in db"); + }; + drop(storage_processor); + + if graph.proceed_withdraw_height <= 0 { + warn!("graph {graph_id} proceed_withdraw_height <= 0, waiting to been updated"); + return Ok((None, get_operator_proof_wait_secs())); + } + + let watchtower_challenge_init_txid = graph + .watchtower_challenge_init_txid + .ok_or_else(|| anyhow::anyhow!("watchtower_challenge_init_txid is none"))?; + let num_challenger = bitvm_graph.parameters.watchtower_pubkeys.len(); + let (watchtower_challenge_txids, included_watchtowers) = match get_watchtower_challenge_info( + btc_client, + &watchtower_challenge_init_txid, + num_challenger, + ) + .await + { + Ok(info) => info, + Err(e) => { + warn!("Failed to get watchtower challenge info: {e}"); + return Ok((None, get_operator_proof_wait_secs())); + } + }; + let (btc_best_block_hash, _) = compute_operator_pubin_blockhash_and_bitmap( + btc_client, + &watchtower_challenge_txids, + &included_watchtowers, + ) + .await?; + let operator_committed_blockhash = BlockHash::from_byte_array(btc_best_block_hash).to_string(); + + let base_url = Url::parse( + &get_proof_build_rpc_host() + .ok_or_else(|| anyhow::anyhow!("failed to get proof_build_rpc_host"))?, + )?; + let operator_url = base_url.join(NODES_OPERATOR_BASE)?; + let operator_response = http_client + .post_response_json::( + operator_url.as_str(), + &OperatorProofRequest { + instance_id: instance_id.to_string(), + graph_id: graph_id.to_string(), + operator_committed_blockhash, + execution_layer_block_number: graph.proceed_withdraw_height, + watchtower_challenge_txids, + included_watchtowers, + watchtower_challenge_init_txid: watchtower_challenge_init_txid.0.to_string(), + watchtower_challenge_pubkeys: bitvm_graph + .parameters + .watchtower_pubkeys + .iter() + .map(|pk| pk.public_key(secp256k1::Parity::Even).to_string()) + .collect(), + }, + ) + .await?; + + let Some(proof_data) = operator_response.proof_data else { + if let Some(error) = operator_response.error { + info!("operator proof is not ready for graph_id:{graph_id}: {error}"); + } + return Ok((None, get_operator_proof_wait_secs())); + }; + + let statement = derive_operator_statement(graph_id)?; + + let proof: ZKMProofWithPublicValues = bincode::deserialize(proof_data.proof.as_slice()) + .map_err(|err| anyhow!("failed to deserialize operator proof: {err}"))?; + + let operator_vk_hash_raw = decode_zkm_vkey_hash(&proof_data.vk) + .map_err(|e| anyhow!("invalid operator proof vk hash: {e:?}"))?; + if operator_vk_hash_raw != statement.vk_hash { + bail!("operator proof vk hash does not match configured operator identity"); + } + if proof.zkm_version != statement.zkm_version || proof_data.zkm_version != statement.zkm_version { + bail!("operator proof Ziren version does not match configured operator identity"); + } + + let outputs = decode_operator_public_outputs(&proof.public_values.to_vec(), statement.vk_hash) + .map_err(|e| anyhow!("invalid operator public outputs: {e}"))?; + if outputs.constant != statement.constant { + bail!("operator proof constant does not match graph setup"); + } + + let part_stark_vk = load_part_stark_vk_for_zkm_version(&proof.zkm_version)?; + let ark_proof = + convert_ark_imm_wrap_vk(&proof, &proof_data.vk, &IMM_GROTH16_VK_BYTES, &part_stark_vk) + .map_err(|e| anyhow!("failed to convert operator proof to ark format: {e}"))?; + if ark_proof.public_inputs[0] != statement.static_input { + bail!("operator proof static public input does not match graph setup statement"); + } + + Ok(( + Some(ValidatedOperatorProof { + proof: ark_proof.proof, + public_inputs: ark_proof.public_inputs.into(), + verifying_key: ark_proof.groth16_vk.into(), + public_values: proof.public_values.to_vec(), + vk_hash: proof_data.vk, + zkm_version: proof.zkm_version, + }), + 0, + )) +} + pub async fn verifier_force_skip_kickoff(client: &BTCClient, graph: &BitvmGcGraph) -> Result { let verifier_master_key = VerifierMasterKey::new(get_bitvm_key()?); let verifier_master_keypair = verifier_master_key.master_keypair(); @@ -4773,3 +4987,121 @@ pub struct OperatorBabeSetupState { #[serde(default)] pub asserted_wrapper_proof: Option>, } + +#[cfg(test)] +mod commit_pubin_tests { + use super::*; + use bitcoin::BlockHash; + use client::btc_chain::BTCClient; + use esplora_client::{Tx, TxStatus, Vin}; + use store::SerializableTxid; + + fn make_txid(byte: u8) -> Txid { + Txid::from_slice(&[byte; 32]).unwrap() + } + + fn make_block_hash(byte: u8) -> BlockHash { + BlockHash::from_slice(&[byte; 32]).unwrap() + } + + fn create_confirmed_tx( + txid: Txid, + spends: &[(Txid, u32)], + height: u32, + block_hash: BlockHash, + ) -> Tx { + Tx { + txid, + version: 2, + locktime: 0, + vin: spends + .iter() + .map(|(prev_txid, vout)| Vin { + txid: *prev_txid, + vout: *vout, + prevout: None, + scriptsig: ScriptBuf::default(), + witness: vec![], + sequence: 1, + is_coinbase: false, + }) + .collect(), + vout: vec![], + size: 1, + weight: 1, + status: TxStatus { + confirmed: true, + block_height: Some(height), + block_hash: Some(block_hash), + block_time: Some(1_000_000), + }, + fee: 0, + } + } + + #[tokio::test] + async fn test_get_watchtower_challenge_info_partial_inclusion() { + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let watchtower_init_txid = make_txid(0x00); + let challenge_txid_wt0 = make_txid(0x01); + let block_hash = make_block_hash(0xAA); + + // watchtower 0 spent vout 0 of the init tx; watchtower 1 did not spend vout 1 + mock_adaptor.set_tx( + challenge_txid_wt0, + create_confirmed_tx(challenge_txid_wt0, &[(watchtower_init_txid, 0)], 100, block_hash), + ); + + let (txids, bits) = get_watchtower_challenge_info( + &btc_client, + &SerializableTxid::from(watchtower_init_txid), + 2, // 2 watchtowers + ) + .await + .unwrap(); + + assert_eq!(txids, vec![Some(challenge_txid_wt0.to_string()), None]); + assert_eq!(bits, vec![true, false]); + } + + #[tokio::test] + async fn test_compute_operator_pubin_blockhash_and_bitmap() { + let (btc_client, mock_adaptor) = BTCClient::new_mock_client(); + + let init_txid = make_txid(0x00); + let challenge_txid_wt0 = make_txid(0x01); + let challenge_txid_wt2 = make_txid(0x02); + let block_hash_low = make_block_hash(0x10); + let block_hash_high = make_block_hash(0x20); + + // wt0 confirmed at height 100, wt2 at height 200 (highest) + mock_adaptor.set_tx( + challenge_txid_wt0, + create_confirmed_tx(challenge_txid_wt0, &[(init_txid, 0)], 100, block_hash_low), + ); + mock_adaptor.set_tx( + challenge_txid_wt2, + create_confirmed_tx(challenge_txid_wt2, &[(init_txid, 2)], 200, block_hash_high), + ); + + let challenge_txids = vec![ + Some(challenge_txid_wt0.to_string()), + None, + Some(challenge_txid_wt2.to_string()), + ]; + let bits = vec![true, false, true]; + + let (best_hash, bitmap) = + compute_operator_pubin_blockhash_and_bitmap(&btc_client, &challenge_txids, &bits) + .await + .unwrap(); + + // block at height 200 wins + assert_eq!(best_hash, block_hash_high.to_byte_array()); + + // bits 0 and 2 set → byte 0 = 0b0000_0101 + assert_eq!(bitmap[0], 0b0000_0101u8); + assert_eq!(&bitmap[1..], &[0u8; 31]); + } +}