diff --git a/Cargo.lock b/Cargo.lock index b2915a70..d3f46811 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1325,7 +1325,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1336,7 +1336,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -3005,7 +3005,7 @@ dependencies = [ [[package]] name = "bitvm" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#fd8a985c16f39f0df26ad46749c266a3fb516d10" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#e369b2a59d86dad2bdc52daeb5fc6b890e9dc812" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -4825,7 +4825,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -5738,7 +5738,7 @@ dependencies = [ [[package]] name = "goat" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#fd8a985c16f39f0df26ad46749c266a3fb516d10" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#e369b2a59d86dad2bdc52daeb5fc6b890e9dc812" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -8183,7 +8183,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11308,7 +11308,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.12.1", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11411,7 +11411,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12168,7 +12168,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -12854,7 +12854,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix 1.1.4", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -14259,7 +14259,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] diff --git a/crates/bitvm-gc/src/committee/api.rs b/crates/bitvm-gc/src/committee/api.rs index d92b3b0e..dfbcc8b2 100644 --- a/crates/bitvm-gc/src/committee/api.rs +++ b/crates/bitvm-gc/src/committee/api.rs @@ -4,7 +4,7 @@ use bitcoin::{ PublicKey, Script, TapLeafHash, TapSighash, TapSighashType, Transaction, TxOut, key::Keypair, sighash::{Prevouts, SighashCache}, - taproot::{LeafVersion, Signature as TaprootSignature}, + taproot::{ControlBlock, LeafVersion, Signature as TaprootSignature}, }; use goat::contexts::base::generate_n_of_n_public_key; use goat::transactions::base::BaseTransaction; @@ -13,7 +13,7 @@ use goat::transactions::pre_signed_musig2::{get_nonce_message, verify_public_non use goat::transactions::signing_musig2::generate_aggregated_nonce; use musig2::secp::Point; use musig2::{AggNonce, KeyAggContext, PartialSignature, PubNonce, SecNonce, verify_partial}; -use secp256k1::schnorr::Signature as SchnorrSignature; +use secp256k1::{Message, SECP256K1, schnorr::Signature as SchnorrSignature}; use serde::{Deserialize, Serialize}; use crate::keys::hkdf_derive_bytes; @@ -411,7 +411,10 @@ fn taproot_script_spend_sighash( prevouts: &[TxOut], leaf_hash: TapLeafHash, sighash_type: TapSighashType, -) -> TapSighash { +) -> Result { + let prevout = prevouts + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("missing prevout for input {input_index}"))?; if sighash_type == TapSighashType::AllPlusAnyoneCanPay || sighash_type == TapSighashType::SinglePlusAnyoneCanPay || sighash_type == TapSighashType::NonePlusAnyoneCanPay @@ -419,11 +422,11 @@ fn taproot_script_spend_sighash( SighashCache::new(tx) .taproot_script_spend_signature_hash( input_index, - &Prevouts::One(input_index, &prevouts[input_index]), + &Prevouts::One(input_index, prevout), leaf_hash, sighash_type, ) - .expect("Failed to construct sighash") + .map_err(|e| anyhow::anyhow!("failed to construct input {input_index} sighash: {e}")) } else { SighashCache::new(tx) .taproot_script_spend_signature_hash( @@ -432,7 +435,7 @@ fn taproot_script_spend_sighash( leaf_hash, sighash_type, ) - .expect("Failed to construct sighash") + .map_err(|e| anyhow::anyhow!("failed to construct input {input_index} sighash: {e}")) } } @@ -457,11 +460,91 @@ pub fn verify_taproot_partial_signature( ) -> Result<()> { let key_agg_ctx = key_agg_context(committee_pubkeys)?; let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); - let sighash = taproot_script_spend_sighash(tx, input_index, prevouts, leaf_hash, sighash_type); + let sighash = taproot_script_spend_sighash(tx, input_index, prevouts, leaf_hash, sighash_type)?; verify_partial(&key_agg_ctx, partial_sig, agg_nonce, committee_pubkey.inner, pub_nonce, sighash) .map_err(|e| anyhow::anyhow!("invalid partial signature from {committee_pubkey}: {e}")) } +pub fn verify_taproot_pre_signed_input( + tx: &T, + signing_pubkey: &XOnlyPublicKey, + input_index: usize, + sighash_type: TapSighashType, +) -> Result<()> { + let input = tx + .tx() + .input + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} is missing", tx.name()))?; + ensure!( + input.witness.len() == 3, + "{} input {input_index} must contain signature, script and control block", + tx.name() + ); + let signature_bytes = input + .witness + .nth(0) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} signature is missing", tx.name()))?; + let witness_script = input + .witness + .nth(1) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} script is missing", tx.name()))?; + let control_block_bytes = input.witness.nth(2).ok_or_else(|| { + anyhow::anyhow!("{} input {input_index} control block is missing", tx.name()) + })?; + let prevout = tx + .prev_outs() + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} prevout is missing", tx.name()))?; + let script = tx + .prev_scripts() + .get(input_index) + .ok_or_else(|| anyhow::anyhow!("{} input {input_index} script is missing", tx.name()))?; + + ensure!( + witness_script == script.as_bytes(), + "{} input {input_index} witness script mismatch", + tx.name() + ); + ensure!( + prevout.script_pubkey.is_p2tr(), + "{} input {input_index} prevout is not P2TR", + tx.name() + ); + let output_key = + XOnlyPublicKey::from_slice(&prevout.script_pubkey.as_bytes()[2..]).map_err(|e| { + anyhow::anyhow!("{} input {input_index} output key is invalid: {e}", tx.name()) + })?; + let control_block = ControlBlock::decode(control_block_bytes).map_err(|e| { + anyhow::anyhow!("{} input {input_index} control block is invalid: {e}", tx.name()) + })?; + ensure!( + control_block.verify_taproot_commitment(SECP256K1, output_key, script), + "{} input {input_index} control block does not commit to the expected script", + tx.name() + ); + + let signature = TaprootSignature::from_slice(signature_bytes).map_err(|e| { + anyhow::anyhow!("{} input {input_index} signature is invalid: {e}", tx.name()) + })?; + ensure!( + signature.sighash_type == sighash_type, + "{} input {input_index} sighash type mismatch", + tx.name() + ); + let leaf_hash = TapLeafHash::from_script(script, LeafVersion::TapScript); + let sighash = taproot_script_spend_sighash( + tx.tx(), + input_index, + tx.prev_outs(), + leaf_hash, + sighash_type, + )?; + SECP256K1.verify_schnorr(&signature.signature, &Message::from(sighash), signing_pubkey).map_err( + |e| anyhow::anyhow!("{} input {input_index} signature verification failed: {e}", tx.name()), + ) +} + #[allow(clippy::too_many_arguments)] fn verify_pre_signed_input( tx: &T, @@ -630,6 +713,50 @@ pub fn verify_graph_committee_partial_sigs( Ok(()) } +pub fn verify_graph_committee_pre_signatures(graph: &BitvmGcGraph) -> Result<()> { + ensure!(graph.committee_pre_signed(), "graph is not pre-signed by the committee"); + let (_, committee_pubkey) = + generate_n_of_n_public_key(&graph.parameters.instance_parameters.committee_pubkeys); + ensure!( + committee_pubkey == graph.parameters.instance_parameters.n_of_n_taproot_public_key(), + "graph committee aggregate public key mismatch" + ); + + verify_taproot_pre_signed_input(&graph.take1, &committee_pubkey, 0, TapSighashType::All)?; + verify_taproot_pre_signed_input(&graph.take1, &committee_pubkey, 3, TapSighashType::All)?; + verify_taproot_pre_signed_input(&graph.take2, &committee_pubkey, 0, TapSighashType::All)?; + verify_taproot_pre_signed_input( + &graph.challenge, + &committee_pubkey, + 0, + TapSighashType::SinglePlusAnyoneCanPay, + )?; + for tx in &graph.watchtower_challenge_timeouts { + verify_taproot_pre_signed_input(tx, &committee_pubkey, 1, TapSighashType::None)?; + } + for tx in &graph.operator_challenge_nacks { + verify_taproot_pre_signed_input(tx, &committee_pubkey, 0, TapSighashType::All)?; + verify_taproot_pre_signed_input(tx, &committee_pubkey, 1, TapSighashType::All)?; + } + verify_taproot_pre_signed_input( + &graph.operator_commit_timeout, + &committee_pubkey, + 0, + TapSighashType::All, + )?; + verify_taproot_pre_signed_input( + &graph.operator_commit_timeout, + &committee_pubkey, + 1, + TapSighashType::All, + )?; + for tx in &graph.disproves { + verify_taproot_pre_signed_input(tx, &committee_pubkey, 0, TapSighashType::None)?; + verify_taproot_pre_signed_input(tx, &committee_pubkey, 1, TapSighashType::None)?; + } + Ok(()) +} + pub fn verify_pegin_confirm_partial_sig( instance_parameters: &BitvmGcInstanceParameters, committee_pubkeys: &[PublicKey], diff --git a/crates/bitvm-gc/src/operator/api.rs b/crates/bitvm-gc/src/operator/api.rs index 3d43a263..563f080a 100644 --- a/crates/bitvm-gc/src/operator/api.rs +++ b/crates/bitvm-gc/src/operator/api.rs @@ -1,6 +1,6 @@ use anyhow::{Result, bail}; -use bitcoin::{Address, Amount, Transaction, TxIn, key::Keypair}; -use bitcoin::{Network, PublicKey, Witness}; +use bitcoin::{Address, Amount, TapSighashType, Transaction, TxIn, key::Keypair}; +use bitcoin::{Network, PublicKey, Witness, XOnlyPublicKey}; use goat::assert_scripts::{ OperatorAssertPublicKey, OperatorAssertSecretKey, OperatorCommitPubinPublicKey, OperatorCommitPubinSecretKey, @@ -26,6 +26,7 @@ use goat::transactions::watchtower_challenge::{ }; use goat::wots::{Wots, Wots96}; +use crate::committee::verify_taproot_pre_signed_input; use crate::keys::hkdf_derive_bytes; use crate::timelocks::{ default_timelock_config, take1_timelock_blocks, take2_timelock_blocks, validate_timelock_config, @@ -393,6 +394,51 @@ pub fn push_operator_pre_signature( Ok(()) } +pub fn verify_graph_operator_pre_signatures(graph: &BitvmGcGraph) -> Result<()> { + if !graph.operator_pre_signed() { + bail!("graph is not pre-signed by the operator"); + } + let operator_pubkey = XOnlyPublicKey::from(graph.parameters.operator_pubkey); + + verify_taproot_pre_signed_input( + &graph.force_skip_kickoff, + &operator_pubkey, + 0, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.force_skip_kickoff, + &operator_pubkey, + 1, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.quick_challenge, + &operator_pubkey, + 0, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.quick_challenge, + &operator_pubkey, + 1, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.challenge_incomplete_kickoff, + &operator_pubkey, + 0, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.challenge_incomplete_kickoff, + &operator_pubkey, + 1, + TapSighashType::None, + )?; + Ok(()) +} + /// remember to sign replensish inputs (if any) after this pub fn operator_sign_prekickoff_input_0( operator_keypair: Keypair, @@ -557,7 +603,8 @@ pub fn operator_sign_assert( graph: &mut BitvmGcGraph, wots_secret_key: &OperatorAssertSecretKey, proof: &[u8; 96], - extra_data: &[u8], + pi2: &[u8], + pi3: &[u8], ) -> Result { if Wots96::generate_public_key(wots_secret_key) != graph.parameters.operator_assert_wots_pubkey { @@ -567,7 +614,7 @@ pub fn operator_sign_assert( let connector_c = graph.connector_c(); graph .operator_assert - .operator_commit_proof(wots_secret_key, &connector_c, proof, extra_data) + .operator_commit_proof(wots_secret_key, &connector_c, proof, pi2, pi3) .map_err(|e| anyhow::anyhow!("failed to sign operator assert: {e}"))?; Ok(graph.operator_assert.tx().clone()) } diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 37f5ea03..c9b4b7c0 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -139,18 +139,21 @@ pub fn verify_prover_assertion(_graph: &BitvmGcGraph, _operator_assert_txin: TxI fn split_operator_assert_wots_and_extra_data( mut operator_assertion: RawWitness, -) -> Result<(RawWitness, Vec)> { - let expected_len = PROVER_SIG_LEN + 1; +) -> Result<(RawWitness, Vec, Vec)> { + let expected_len = PROVER_SIG_LEN + 2; if operator_assertion.len() != expected_len { bail!( "operator assert witness has {} stack items; expected {expected_len}", operator_assertion.len() ); } - let extra_data = operator_assertion + let pi3 = operator_assertion .pop() .ok_or_else(|| anyhow::anyhow!("operator assert witness is empty"))?; - Ok((operator_assertion, extra_data)) + let pi2 = operator_assertion + .pop() + .ok_or_else(|| anyhow::anyhow!("operator assert witness is missing pi2"))?; + Ok((operator_assertion, pi2, pi3)) } fn operator_assert_wots_signature(operator_assertion: &RawWitness) -> Result> { @@ -191,14 +194,16 @@ fn operator_assert_wots_signature(operator_assertion: &RawWitness) -> Result) -> Result<(Vec, Vec)> { +fn validate_operator_assert_extra_data(pi2: &[u8], pi3: &[u8]) -> Result<()> { let pi2_len = G2Affine::default().compressed_size(); let pi3_len = G1Affine::default().compressed_size(); - let expected_len = pi2_len + pi3_len; - if extra_data.len() != expected_len { - bail!("operator assert extra-data has {} bytes; expected {expected_len}", extra_data.len()); + if pi2.len() != pi2_len { + bail!("operator assert pi2 has {} bytes; expected {pi2_len}", pi2.len()); + } + if pi3.len() != pi3_len { + bail!("operator assert pi3 has {} bytes; expected {pi3_len}", pi3.len()); } - Ok((extra_data[..pi2_len].to_vec(), extra_data[pi2_len..].to_vec())) + Ok(()) } pub fn extract_operator_assert_witness( @@ -210,11 +215,11 @@ pub fn extract_operator_assert_witness( .connector_c() .extract_leaf_1_raw_witness(operator_assert_txin) .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; - let (operator_assertion, extra_data) = + let (operator_assertion, pi2, pi3) = split_operator_assert_wots_and_extra_data(operator_assertion)?; let wots_sig = operator_assert_wots_signature(&operator_assertion)?; - let (pi2, pi3) = split_operator_assert_extra_data(extra_data)?; + validate_operator_assert_extra_data(&pi2, &pi3)?; let assert_witness = TxAssertWitness { wots_sig, pi2, pi3 }; if assert_witness.recover_pi2_pi3().is_none() { bail!("operator assert extra-data cannot recover pi2 and pi3"); @@ -231,11 +236,14 @@ pub fn extract_operator_assert_witness_for_challenge( .connector_c() .extract_leaf_1_raw_witness(operator_assert_txin) .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; - let (operator_assertion, extra_data) = + let (operator_assertion, pi2, pi3) = split_operator_assert_wots_and_extra_data(operator_assertion)?; let wots_sig = operator_assert_wots_signature(&operator_assertion)?; - let (pi2, pi3) = - split_operator_assert_extra_data(extra_data).unwrap_or_else(|_| (Vec::new(), Vec::new())); + let (pi2, pi3) = if validate_operator_assert_extra_data(&pi2, &pi3).is_ok() { + (pi2, pi3) + } else { + (Vec::new(), Vec::new()) + }; Ok(TxAssertWitness { wots_sig, pi2, pi3 }) } @@ -253,7 +261,7 @@ pub fn build_verifier_assert_tx( let operator_assertion = connector_c .extract_leaf_1_raw_witness(&operator_assert_txin) .map_err(|e| anyhow::anyhow!("failed to extract operator assertion: {e}"))?; - let (operator_assertion, _extra_data) = + let (operator_assertion, _pi2, _pi3) = split_operator_assert_wots_and_extra_data(operator_assertion)?; let verifier_connector = graph.verifier_connector(verifier_index)?; @@ -320,7 +328,7 @@ pub fn validate_pubin_disprove( let operator_assert_witness = connector_c .extract_leaf_1_raw_witness(operator_assert_txin) .map_err(|e| anyhow::anyhow!("failed to extract operator assert witness: {e}"))?; - let (operator_assert_witness, _extra_data) = + let (operator_assert_witness, _pi2, _pi3) = split_operator_assert_wots_and_extra_data(operator_assert_witness)?; let connector_d = graph.connector_d(); let input_lock_script = diff --git a/crates/client/src/goat_chain/chain_adaptor.rs b/crates/client/src/goat_chain/chain_adaptor.rs index f962d5bb..3fc6ac00 100644 --- a/crates/client/src/goat_chain/chain_adaptor.rs +++ b/crates/client/src/goat_chain/chain_adaptor.rs @@ -362,7 +362,7 @@ pub struct WithdrawData { pub btc_block_height_withdraw: U256, } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct GraphData { pub operator_pubkey_prefix: u8, pub operator_pubkey: [u8; 32], diff --git a/node/src/handle.rs b/node/src/handle.rs index 0bd345f5..1d3f10e5 100644 --- a/node/src/handle.rs +++ b/node/src/handle.rs @@ -622,6 +622,12 @@ fn freeze_operator_candidates(state: &mut OperatorBabeSetupState) -> Result<()> todo_funcs::min_required_verifier() ); } + let mut verifier_peer_ids = std::collections::HashSet::new(); + for candidate in &state.candidates { + if !verifier_peer_ids.insert(&candidate.verifier_peer_id) { + bail!("cannot freeze duplicate verifier peer id"); + } + } state.candidates.truncate(todo_funcs::min_required_verifier()); state.candidates.sort_by_key(|candidate| candidate.verifier_pubkey.to_bytes()); for (verifier_index, candidate) in state.candidates.iter_mut().enumerate() { @@ -1266,6 +1272,20 @@ async fn handle_gen_circuits_operator( bail!("GenCircuits setup package has no commitments"); } + let verifier_peer_id = ctx.from_peer_id.to_bytes(); + if !ctx.goat_client.committee_mana_is_verifier(&verifier_peer_id).await.with_context(|| { + format!( + "failed to validate GenCircuits sender {} against the verifier registry", + ctx.from_peer_id + ) + })? { + tracing::warn!( + "Ignore GenCircuits for {instance_id}:{graph_id}: sender {} is not a registered verifier", + ctx.from_peer_id + ); + return Ok(()); + } + let mut state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)?.unwrap_or_default(); let operator_state = state.operator.get_or_insert_with(|| OperatorBabeSetupState { frozen_verifier_pubkeys: None, @@ -1277,11 +1297,27 @@ async fn handle_gen_circuits_operator( if let Some(existing) = operator_state .candidates .iter() - .find(|candidate| candidate.verifier_pubkey == *verifier_pubkey) + .find(|candidate| candidate.verifier_peer_id == verifier_peer_id) { + if existing.verifier_pubkey != *verifier_pubkey { + tracing::warn!( + "Ignore GenCircuits for {instance_id}:{graph_id}: verifier peer {} already submitted a different public key", + ctx.from_peer_id + ); + return Ok(()); + } if existing.setup_package != *setup_package { bail!("conflicting GenCircuits setup package for verifier {verifier_pubkey}"); } + } else if operator_state + .candidates + .iter() + .any(|candidate| candidate.verifier_pubkey == *verifier_pubkey) + { + tracing::warn!( + "Ignore GenCircuits for {instance_id}:{graph_id}: verifier public key {verifier_pubkey} is already bound to another peer" + ); + return Ok(()); } else if was_frozen { tracing::debug!( "Ignore GenCircuits for {instance_id}:{graph_id}: verifier membership is frozen" @@ -1290,6 +1326,7 @@ async fn handle_gen_circuits_operator( return Ok(()); } else { operator_state.candidates.push(OperatorVerifierCandidate { + verifier_peer_id, verifier_pubkey: *verifier_pubkey, setup_package: setup_package.clone(), verifier_index: None, @@ -1497,6 +1534,14 @@ async fn handle_soldering_proof_ready_operator( if candidate.verifier_index != Some(verifier_index) { bail!("selected verifier candidate index does not match SolderingProofReady slot"); } + let verifier_peer_id = ctx.from_peer_id.to_bytes(); + if candidate.verifier_peer_id != verifier_peer_id { + tracing::warn!( + "Ignore SolderingProofReady for {instance_id}:{graph_id}: sender {} does not own verifier slot {verifier_index}", + ctx.from_peer_id + ); + return Ok(()); + } let store_base_path = get_soldering_proof_payload_store_path()?; let payload_path = soldering_proof_payload_store_path( @@ -1784,9 +1829,7 @@ async fn handle_create_graph_committee( ) -> Result<()> { // received from Operator // 1. check graph data & operator stake - if let Err(e) = - todo_funcs::validate_init_graph(ctx.local_db, ctx.btc_client, ctx.goat_client, graph).await - { + if let Err(e) = todo_funcs::validate_init_graph(ctx.btc_client, ctx.goat_client, graph).await { if should_ignore_invalid_graph(&e, instance_id, graph_id, "CreateGraph", None) { return Ok(()); } @@ -2438,7 +2481,9 @@ async fn handle_graph_finalize_committee( ) -> Result<()> { // received from Operator // 1. check graph data - if let Err(e) = todo_funcs::validate_finalized_graph(ctx.goat_client, graph, endorse_sigs).await + if let Err(e) = + todo_funcs::validate_finalized_graph(ctx.btc_client, ctx.goat_client, graph, endorse_sigs) + .await { if should_ignore_invalid_graph( &e, @@ -2546,7 +2591,9 @@ async fn handle_graph_finalize_default( ) -> Result<()> { // received from Operator // 1. check graph data - if let Err(e) = todo_funcs::validate_finalized_graph(ctx.goat_client, graph, endorse_sigs).await + if let Err(e) = + todo_funcs::validate_finalized_graph(ctx.btc_client, ctx.goat_client, graph, endorse_sigs) + .await { if should_ignore_invalid_graph( &e, @@ -4021,10 +4068,6 @@ async fn handle_assert_ready_operator( let assert_witness = build_assert_witness(&operator_proof.proof, &assert_secret_key, dynamic_input)?; let assert_message = assert_wots_message(&assert_witness)?; - let mut assert_extra_data = - Vec::with_capacity(assert_witness.pi2.len() + assert_witness.pi3.len()); - assert_extra_data.extend_from_slice(&assert_witness.pi2); - assert_extra_data.extend_from_slice(&assert_witness.pi3); let mut asserted_operator_proof = Vec::new(); operator_proof.proof.serialize_compressed(&mut asserted_operator_proof)?; let mut setup_state = load_babe_setup_state(ctx.local_db, instance_id, graph_id)? @@ -4041,8 +4084,13 @@ async fn handle_assert_ready_operator( operator_state.asserted_operator_proof = Some(asserted_operator_proof); save_babe_setup_state(ctx.local_db, instance_id, graph_id, &setup_state)?; - let assert_tx = - operator_sign_assert(&mut graph, &assert_secret_key, &assert_message, &assert_extra_data)?; + let assert_tx = operator_sign_assert( + &mut graph, + &assert_secret_key, + &assert_message, + &assert_witness.pi2, + &assert_witness.pi3, + )?; let assert_tx_total_input_amount = graph.operator_assert.prev_outs().iter().map(|o| o.value).sum::(); broadcast_tx_with_cpfp(ctx.btc_client, assert_tx, assert_tx_total_input_amount).await?; @@ -5040,13 +5088,75 @@ async fn handle_sync_graph( ); return Ok(()); } + + if !ctx + .goat_client + .committee_mana_is_validate_peer_id(&ctx.from_peer_id.to_bytes()) + .await + .with_context(|| { + format!( + "failed to validate SyncGraph sender {} against the committee registry", + ctx.from_peer_id + ) + })? + { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: sender {} is not a registered committee peer", + ctx.from_peer_id + ); + return Ok(()); + } + + if graph.parameters.instance_parameters.instance_id != instance_id + || graph.parameters.graph_id != graph_id + { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: message identifiers do not match graph parameters" + ); + return Ok(()); + } + validate_graph_id_on_goat(ctx.goat_client, instance_id, graph_id).await.map_err(|e| { anyhow!( "Failed to validate graph_id on GoatChain for SyncGraph {instance_id}:{graph_id}: {e}" ) })?; - store_graph(ctx.local_db, graph).await?; + if let Err(e) = todo_funcs::validate_graph_instance_parameters( + ctx.btc_client, + ctx.goat_client, + &graph.parameters.instance_parameters, + ) + .await + { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: invalid instance parameters: {e}" + ); + return Ok(()); + } let graph = BitvmGcGraph::from_simplified(graph)?; + let graph_data = build_graph_data(&graph)?; + let graph_data_on_goat = ctx.goat_client.gateway_get_graph_data(&graph_id).await?; + if graph_data != graph_data_on_goat { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: reconstructed graph data does not match GoatChain" + ); + return Ok(()); + } + + if let Err(e) = verify_graph_operator_pre_signatures(&graph) { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: invalid operator pre-signatures: {e}" + ); + return Ok(()); + } + if let Err(e) = verify_graph_committee_pre_signatures(&graph) { + tracing::warn!( + "Ignore SyncGraph for {instance_id}:{graph_id}: invalid committee pre-signatures: {e}" + ); + return Ok(()); + } + let simplified_graph = graph.to_simplified()?; + store_graph(ctx.local_db, &simplified_graph).await?; refresh_and_compensate( ctx, instance_id, @@ -5113,6 +5223,7 @@ mod tests { OperatorBabeSetupState { frozen_verifier_pubkeys: Some(vec![verifier_pubkey()]), candidates: vec![OperatorVerifierCandidate { + verifier_peer_id: vec![1], verifier_pubkey: verifier_pubkey(), setup_package: package, verifier_index: Some(0), @@ -5130,6 +5241,7 @@ mod tests { let mut state = OperatorBabeSetupState { frozen_verifier_pubkeys: None, candidates: vec![OperatorVerifierCandidate { + verifier_peer_id: vec![1], verifier_pubkey: verifier_pubkey(), setup_package: package, verifier_index: None, diff --git a/node/src/utils.rs b/node/src/utils.rs index 2b1c30f4..52e4f640 100644 --- a/node/src/utils.rs +++ b/node/src/utils.rs @@ -206,45 +206,114 @@ pub mod todo_funcs { Ok(()) } - pub async fn validate_init_graph( - local_db: &LocalDB, + pub async fn validate_graph_instance_parameters( btc_client: &BTCClient, goat_client: &GOATClient, - graph: &SimplifiedBitvmGcGraph, + parameters: &BitvmGcInstanceParameters, ) -> Result<()> { - // Basic structural and on-chain consistency checks for an incoming graph proposal. - // Return SpecialError::InvalidGraph on any validation failure. - // 1) Rebuild full graph (ensures signatures present if flags are set and tx graph is coherent) - let full_graph = BitvmGcGraph::from_simplified(graph) - .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + let expected = super::read_instance_info_from_goat(goat_client, parameters.instance_id) + .await + .map_err(|e| { + SpecialError::InvalidGraph(format!( + "failed to load instance parameters from GoatChain: {e}" + )) + })?; + if parameters != &expected { + bail!(SpecialError::InvalidGraph( + "instance parameters mismatch with GoatChain peg-in data".to_string() + )); + } - // 2) Network must match local node network - let net = get_network(); - if graph.parameters.instance_parameters.network != net { + for input in &expected.user_info.inputs { + let funding_tx = btc_client.get_tx(&input.outpoint.txid).await.map_err(|e| { + SpecialError::InvalidGraph(format!( + "failed to load peg-in funding transaction {}: {e}", + input.outpoint.txid + )) + })?; + let Some(funding_tx) = funding_tx else { + bail!(SpecialError::InvalidGraph(format!( + "peg-in funding transaction {} not found on Bitcoin", + input.outpoint.txid + ))); + }; + let funding_output = + funding_tx.output.get(input.outpoint.vout as usize).ok_or_else(|| { + SpecialError::InvalidGraph(format!( + "peg-in funding outpoint {}:{} does not exist", + input.outpoint.txid, input.outpoint.vout + )) + })?; + if funding_output.value != input.amount { + bail!(SpecialError::InvalidGraph(format!( + "peg-in funding amount mismatch for {}:{}: graph={}, bitcoin={}", + input.outpoint.txid, + input.outpoint.vout, + input.amount.to_sat(), + funding_output.value.to_sat() + ))); + } + } + + let pegin_deposit_txid = expected + .build_pegin_tx() + .map_err(|e| { + SpecialError::InvalidGraph(format!("failed to reconstruct peg-in deposit: {e}")) + })? + .0 + .tx() + .compute_txid(); + if btc_client + .get_tx(&pegin_deposit_txid) + .await + .map_err(|e| { + SpecialError::InvalidGraph(format!( + "failed to load peg-in deposit transaction {pegin_deposit_txid}: {e}" + )) + })? + .is_none() + { bail!(SpecialError::InvalidGraph(format!( - "network mismatch: graph={:?} local={:?}", - graph.parameters.instance_parameters.network, net + "peg-in deposit transaction {pegin_deposit_txid} not found on Bitcoin" ))); } - // 3) Committee pubkeys must match what's registered on GoatChain for this instance - let instance_id = graph.parameters.instance_parameters.instance_id; - let committee_on_chain = - goat_client.gateway_get_committee_pubkeys(&instance_id).await.map_err(|e| { - SpecialError::InvalidGraph(format!("failed to load committee from chain: {e}")) - })?; - if committee_on_chain != graph.parameters.instance_parameters.committee_pubkeys { + Ok(()) + } + + pub async fn validate_init_graph( + btc_client: &BTCClient, + goat_client: &GOATClient, + graph: &SimplifiedBitvmGcGraph, + ) -> Result<()> { + if !graph.operator_pre_signed() { bail!(SpecialError::InvalidGraph( - "committee pubkeys mismatch with GoatChain".to_string() + "graph is missing operator pre-signatures".to_string() )); } + // Basic structural and on-chain consistency checks for an incoming graph proposal. + // Return SpecialError::InvalidGraph on any validation failure. + // 1) Rebuild full graph (ensures signatures present if flags are set and tx graph is coherent) + let full_graph = BitvmGcGraph::from_simplified(graph) + .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + verify_graph_operator_pre_signatures(&full_graph).map_err(|e| { + SpecialError::InvalidGraph(format!("invalid operator pre-signatures: {e}")) + })?; + + // 2) Bind all instance and peg-in parameters to GoatChain and Bitcoin. + validate_graph_instance_parameters( + btc_client, + goat_client, + &graph.parameters.instance_parameters, + ) + .await?; - // 4) Challenge amount and assert-commit count must match local constants + // 3) Challenge amount and assert-commit count must match local constants if graph.parameters.challenge_amount != super::todo_funcs::challenge_amount() { bail!(SpecialError::InvalidGraph("unexpected challenge amount".to_string())); } - // 5) Watchtower config sanity: number of watchtowers should match number of hashlocks and registry size + // 4) Watchtower config sanity: number of watchtowers should match number of hashlocks and registry size let watchtowers_on_chain = goat_client.committee_mana_get_watchtowers().await.map_err(|e| { SpecialError::InvalidGraph(format!("failed to load watchtowers from chain: {e}")) @@ -258,7 +327,7 @@ pub mod todo_funcs { graph.parameters.pubin_disprove_constant, )?; - // 6) Operator stake sanity: verify operator is registered and has enough locked stake + // 5) Operator stake sanity: verify operator is registered and has enough locked stake let op_pk_bytes = graph.parameters.operator_pubkey.to_bytes(); let xonly: [u8; 32] = op_pk_bytes[1..33] .try_into() @@ -286,32 +355,35 @@ pub mod todo_funcs { Ok(()) } pub async fn validate_finalized_graph( + btc_client: &BTCClient, goat_client: &GOATClient, graph: &SimplifiedBitvmGcGraph, endorse_sigs: &[(PublicKey, EvmAddress, Vec)], ) -> Result<()> { + if !graph.operator_pre_signed() || !graph.committee_pre_signed() { + bail!(SpecialError::InvalidGraph( + "finalized graph is missing operator or committee pre-signatures".to_string() + )); + } // 1) Rebuild full graph to ensure structure is coherent and txns derivable let full_graph = BitvmGcGraph::from_simplified(graph) .map_err(|e| SpecialError::InvalidGraph(format!("invalid graph structure: {e}")))?; + verify_graph_operator_pre_signatures(&full_graph).map_err(|e| { + SpecialError::InvalidGraph(format!("invalid operator pre-signatures: {e}")) + })?; + verify_graph_committee_pre_signatures(&full_graph).map_err(|e| { + SpecialError::InvalidGraph(format!("invalid committee pre-signatures: {e}")) + })?; + + // 2) Repeat the full instance and peg-in binding for nodes that did not see CreateGraph. + validate_graph_instance_parameters( + btc_client, + goat_client, + &graph.parameters.instance_parameters, + ) + .await?; - // 2) Repeat key static checks (network, committee set, counts) - let net = get_network(); - if graph.parameters.instance_parameters.network != net { - bail!(SpecialError::InvalidGraph(format!( - "network mismatch: graph={:?} local={:?}", - graph.parameters.instance_parameters.network, net - ))); - } let instance_id = graph.parameters.instance_parameters.instance_id; - let committee_on_chain = - goat_client.gateway_get_committee_pubkeys(&instance_id).await.map_err(|e| { - SpecialError::InvalidGraph(format!("failed to load committee from chain: {e}")) - })?; - if committee_on_chain != graph.parameters.instance_parameters.committee_pubkeys { - bail!(SpecialError::InvalidGraph( - "committee pubkeys mismatch with GoatChain".to_string() - )); - } if graph.parameters.challenge_amount != super::todo_funcs::challenge_amount() { bail!(SpecialError::InvalidGraph("unexpected challenge amount".to_string())); } @@ -335,6 +407,18 @@ pub mod todo_funcs { let pegin_data = goat_client.gateway_get_pegin_data(&instance_id).await.map_err(|e| { SpecialError::InvalidGraph(format!("failed to load instance data: {e}")) })?; + if pegin_data.committee_pubkeys.len() != pegin_data.committee_addresses.len() { + bail!(SpecialError::InvalidGraph( + "on-chain committee pubkey and address counts differ".to_string() + )); + } + if endorse_sigs.len() != pegin_data.committee_pubkeys.len() { + bail!(SpecialError::InvalidGraph(format!( + "endorsement count {} does not match instance committee count {}", + endorse_sigs.len(), + pegin_data.committee_pubkeys.len() + ))); + } for (pk, evm_addr, sig) in endorse_sigs.iter() { // no duplicates @@ -5107,6 +5191,7 @@ pub struct VerifierBabeSetupState { #[derive(Clone, Serialize, Deserialize)] pub struct OperatorVerifierCandidate { + pub verifier_peer_id: Vec, pub verifier_pubkey: PublicKey, pub setup_package: CACSetupPackage, pub verifier_index: Option,