diff --git a/Cargo.lock b/Cargo.lock index 4ec9cb8da..b0ff8477c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1300,7 +1300,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]] @@ -1311,7 +1311,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -2710,7 +2710,7 @@ dependencies = [ "bitflags 2.11.0", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools 0.10.5", "log", "prettyplease", "proc-macro2", @@ -2941,7 +2941,7 @@ dependencies = [ [[package]] name = "bitvm" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#bab46c2336ad98513bcb8cbaff21b5506a4dcd9a" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip#c24f6cc2fb99d61f38b082202c899cf368abdae6" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -2983,7 +2983,7 @@ dependencies = [ "bincode", "bitcoin", "bitcoin-script", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip)", "chacha20poly1305", "clap", "esplora-client", @@ -3029,7 +3029,7 @@ dependencies = [ "bitcoin", "bitcoin-light-client-circuit", "bitcoin-script", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip)", "bitvm-gc", "blake3", "borsh", @@ -4722,7 +4722,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]] @@ -5312,7 +5312,7 @@ dependencies = [ [[package]] name = "goat" version = "0.1.0" -source = "git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2#bab46c2336ad98513bcb8cbaff21b5506a4dcd9a" +source = "git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip#c24f6cc2fb99d61f38b082202c899cf368abdae6" dependencies = [ "ark-bn254", "ark-crypto-primitives", @@ -5327,7 +5327,7 @@ dependencies = [ "bitcoin", "bitcoin-script", "bitcoin-scriptexec", - "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=gc-v2)", + "bitvm 0.1.0 (git+https://github.com/GOATNetwork/BitVM.git?branch=committee-force-skip)", "blake3", "clap", "colored", @@ -7705,7 +7705,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]] @@ -10680,7 +10680,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -10771,7 +10771,7 @@ dependencies = [ "security-framework 3.7.0", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -11504,7 +11504,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]] @@ -12151,7 +12151,7 @@ dependencies = [ "getrandom 0.4.2", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -13451,7 +13451,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/Cargo.toml b/Cargo.toml index 4e7e611c7..b270c52ff 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,8 +29,8 @@ members = [ default-members = ["node"] [workspace.dependencies] -bitvm = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "gc-v2" } -goat = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "gc-v2" } +bitvm = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "committee-force-skip" } +goat = { git = "https://github.com/GOATNetwork/BitVM.git", branch = "committee-force-skip" } libp2p = { version = "0.57.1", features = ["tokio", "kad", "noise", "tcp", "yamux", "rsa", "ping"] } libp2p-swarm-derive = "0.36.0" diff --git a/crates/bitvm-gc/src/committee/api.rs b/crates/bitvm-gc/src/committee/api.rs index 5e8a2d03f..962dadba2 100644 --- a/crates/bitvm-gc/src/committee/api.rs +++ b/crates/bitvm-gc/src/committee/api.rs @@ -45,6 +45,12 @@ pub fn operator_challenge_nack_pre_sign_num(watchtower_num: usize) -> usize { pub fn operator_commit_timeout_pre_sign_num() -> usize { 2 } +pub fn force_skip_kickoff_pre_sign_num() -> usize { + 1 +} +pub fn quick_challenge_pre_sign_num() -> usize { + 1 +} pub fn disprove_pre_sign_num(verifier_num: usize) -> usize { verifier_num * 2 } @@ -138,6 +144,10 @@ pub struct CommitteeMusig2Data { pub operator_challenge_nack: Vec, pub operator_commit_timeout: Vec, pub disprove: Vec, + /// Input 1 (the force-skip connector) of Force-Skip-Kickoff. + pub force_skip_kickoff: Vec, + /// Input 1 (the force-skip connector) of Quick-Challenge. + pub quick_challenge: Vec, } pub type CommitteeSecNonces = CommitteeMusig2Data; @@ -170,6 +180,14 @@ impl CommitteeMusig2Data { self.disprove.len() == disprove_pre_sign_num(verifier_num), "invalid number of disprove" ); + ensure!( + self.force_skip_kickoff.len() == force_skip_kickoff_pre_sign_num(), + "invalid number of force skip kickoff" + ); + ensure!( + self.quick_challenge.len() == quick_challenge_pre_sign_num(), + "invalid number of quick challenge" + ); Ok(()) } @@ -182,6 +200,8 @@ impl CommitteeMusig2Data { operator_challenge_nack: vec![], operator_commit_timeout: vec![], disprove: vec![], + force_skip_kickoff: vec![], + quick_challenge: vec![], } } } @@ -287,6 +307,20 @@ pub fn committee_presign_sighash_digest(graph: &BitvmGcGraph) -> Result<[u8; 32] TapSighashType::None, )?; } + add_sighash( + &mut hasher, + b"force_skip_kickoff/1/none", + &graph.force_skip_kickoff, + 1, + TapSighashType::None, + )?; + add_sighash( + &mut hasher, + b"quick_challenge/1/none", + &graph.quick_challenge, + 1, + TapSighashType::None, + )?; Ok(hasher.finalize().into()) } @@ -438,6 +472,26 @@ pub fn committee_pre_sign( res.disprove = disprove_sigs; } + { + // force skip kickoff + let sec_nonces = committee_member_sec_nonce.force_skip_kickoff.try_into().unwrap(); + let agg_nonces = committee_agg_nonce.force_skip_kickoff.try_into().unwrap(); + match graph.force_skip_kickoff.pre_sign(&committee_context, &sec_nonces, &agg_nonces) { + Ok(v) => res.force_skip_kickoff = v.to_vec(), + Err(e) => bail!("fail to pre-sign {}: {e}", graph.force_skip_kickoff.name()), + }; + } + + { + // quick challenge + let sec_nonces = committee_member_sec_nonce.quick_challenge.try_into().unwrap(); + let agg_nonces = committee_agg_nonce.quick_challenge.try_into().unwrap(); + match graph.quick_challenge.pre_sign(&committee_context, &sec_nonces, &agg_nonces) { + Ok(v) => res.quick_challenge = v.to_vec(), + Err(e) => bail!("fail to pre-sign {}: {e}", graph.quick_challenge.name()), + }; + } + Ok(res) } @@ -480,6 +534,8 @@ pub fn nonces_aggregation(pub_nonces_vec: &[CommitteePubNonces]) -> Result res.force_skip_kickoff = v.to_vec(), + Err(e) => bail!("fail to aggregate pre-sigs {}: {e}", graph.force_skip_kickoff.name()), + }; + + // quick challenge + let quick_challenge_agg_nonces = agg_nonces.quick_challenge.clone().try_into().unwrap(); + let quick_challenge_partial_sigs = + [partial_sigs.iter().map(|r| r.quick_challenge[0]).collect()]; + match graph.quick_challenge.aggregate_pre_sigs( + &context, + &quick_challenge_partial_sigs, + &quick_challenge_agg_nonces, + ) { + Ok(v) => res.quick_challenge = v.to_vec(), + Err(e) => bail!("fail to aggregate pre-sigs {}: {e}", graph.quick_challenge.name()), + }; + Ok(res) } @@ -905,6 +987,27 @@ pub fn verify_graph_committee_partial_sigs( )?; } + verify_pre_signed_input( + &graph.force_skip_kickoff, + committee_pubkeys, + committee_pubkey, + &pub_nonces.force_skip_kickoff[0], + partial_sigs.force_skip_kickoff[0], + &agg_nonces.force_skip_kickoff[0], + 1, + TapSighashType::None, + )?; + verify_pre_signed_input( + &graph.quick_challenge, + committee_pubkeys, + committee_pubkey, + &pub_nonces.quick_challenge[0], + partial_sigs.quick_challenge[0], + &agg_nonces.quick_challenge[0], + 1, + TapSighashType::None, + )?; + Ok(()) } @@ -950,6 +1053,18 @@ pub fn verify_graph_committee_pre_signatures(graph: &BitvmGcGraph) -> Result<()> verify_taproot_pre_signed_input(tx, &committee_pubkey, 0, TapSighashType::None)?; verify_taproot_pre_signed_input(tx, &committee_pubkey, 1, TapSighashType::None)?; } + verify_taproot_pre_signed_input( + &graph.force_skip_kickoff, + &committee_pubkey, + 1, + TapSighashType::None, + )?; + verify_taproot_pre_signed_input( + &graph.quick_challenge, + &committee_pubkey, + 1, + TapSighashType::None, + )?; Ok(()) } @@ -1040,6 +1155,15 @@ pub fn push_committee_pre_signatures( ); } + // force skip kickoff and quick challenge + let force_skip_connector = graph.parameters.force_skip_connector(); + graph + .force_skip_kickoff + .push_pre_sigs(&force_skip_connector, sigs.force_skip_kickoff.clone().try_into().unwrap()); + graph + .quick_challenge + .push_pre_sigs(&force_skip_connector, sigs.quick_challenge.clone().try_into().unwrap()); + graph.committee_pre_signed = true; Ok(()) } @@ -1138,6 +1262,28 @@ pub fn generate_nonce_from_seed( index += 1; } } + { + // force skip kickoff + for _ in 0..force_skip_kickoff_pre_sign_num() { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.force_skip_kickoff.push(pub_nonce); + sec_nonces.force_skip_kickoff.push(sec_nonce); + nonce_sigs.force_skip_kickoff.push(nonce_sig); + index += 1; + } + } + { + // quick challenge + for _ in 0..quick_challenge_pre_sign_num() { + let (sec_nonce, pub_nonce, nonce_sig) = + generate_nonce(signer_keypair, &graph_seed, index); + pub_nonces.quick_challenge.push(pub_nonce); + sec_nonces.quick_challenge.push(sec_nonce); + nonce_sigs.quick_challenge.push(nonce_sig); + index += 1; + } + } (pub_nonces, sec_nonces, nonce_sigs) } @@ -1176,7 +1322,9 @@ pub fn verify_nonce_signatures( &pub_nonces.operator_commit_timeout, &nonce_sigs.operator_commit_timeout, ) - && verify_vec(pubkey, &pub_nonces.disprove, &nonce_sigs.disprove)) + && verify_vec(pubkey, &pub_nonces.disprove, &nonce_sigs.disprove) + && verify_vec(pubkey, &pub_nonces.force_skip_kickoff, &nonce_sigs.force_skip_kickoff) + && verify_vec(pubkey, &pub_nonces.quick_challenge, &nonce_sigs.quick_challenge)) } pub(crate) fn generate_nonce( diff --git a/crates/bitvm-gc/src/operator/api.rs b/crates/bitvm-gc/src/operator/api.rs index d0ed018b7..de9ffc0dc 100644 --- a/crates/bitvm-gc/src/operator/api.rs +++ b/crates/bitvm-gc/src/operator/api.rs @@ -65,7 +65,7 @@ pub fn generate_commit_pubin_wots_key( } pub fn operator_presig_num() -> usize { - 6 + 4 } pub fn generate_bitvm_graph(params: BitvmGcGraphParameters) -> Result { @@ -340,24 +340,15 @@ pub fn operator_pre_sign( // presign force_skip_kickoff let kickoff_connector = graph.parameters.kickoff_connector(); - let next_force_skip_connector = graph.parameters.force_skip_connector(); - graph.force_skip_kickoff.pre_sign_and_push( - &context, - &kickoff_connector, - &next_force_skip_connector, - ); + // Input 1 of both penalties spends the force-skip connector and is signed by + // the committee. + graph.force_skip_kickoff.pre_sign_and_push(&context, &kickoff_connector); wits.push(graph.force_skip_kickoff.tx().input[0].witness.clone()); - wits.push(graph.force_skip_kickoff.tx().input[1].witness.clone()); // presign quick_challenge let guardian_connector = graph.guardian_connector(); - graph.quick_challenge.pre_sign_and_push( - &context, - &guardian_connector, - &next_force_skip_connector, - ); + graph.quick_challenge.pre_sign_and_push(&context, &guardian_connector); wits.push(graph.quick_challenge.tx().input[0].witness.clone()); - wits.push(graph.quick_challenge.tx().input[1].witness.clone()); // presign challenge_incomplete_kickoff let next_prekickoff_connector = graph.parameters.prekickoff_connector(); @@ -385,11 +376,9 @@ pub fn push_operator_pre_signature( }; graph.force_skip_kickoff.tx_mut().input[0].witness = signed_witness[0].clone(); - graph.force_skip_kickoff.tx_mut().input[1].witness = signed_witness[1].clone(); - graph.quick_challenge.tx_mut().input[0].witness = signed_witness[2].clone(); - graph.quick_challenge.tx_mut().input[1].witness = signed_witness[3].clone(); - graph.challenge_incomplete_kickoff.tx_mut().input[0].witness = signed_witness[4].clone(); - graph.challenge_incomplete_kickoff.tx_mut().input[1].witness = signed_witness[5].clone(); + graph.quick_challenge.tx_mut().input[0].witness = signed_witness[1].clone(); + graph.challenge_incomplete_kickoff.tx_mut().input[0].witness = signed_witness[2].clone(); + graph.challenge_incomplete_kickoff.tx_mut().input[1].witness = signed_witness[3].clone(); graph.operator_pre_signed = true; Ok(()) @@ -407,24 +396,12 @@ pub fn verify_graph_operator_pre_signatures(graph: &BitvmGcGraph) -> Result<()> 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, diff --git a/crates/bitvm-gc/src/types.rs b/crates/bitvm-gc/src/types.rs index 829725b7d..dadecdfcf 100644 --- a/crates/bitvm-gc/src/types.rs +++ b/crates/bitvm-gc/src/types.rs @@ -387,8 +387,10 @@ impl BitvmGcGraphParameters { PrekickoffConnector::new(self.network(), &self.operator_taproot_public_key()) } + /// Output 0 of the next Pre-Kickoff. It is locked to this graph's committee, + /// which pre-signs its spend in Quick-Challenge and Force-Skip-Kickoff. pub fn force_skip_connector(&self) -> ForceSkipConnector { - ForceSkipConnector::new(self.network(), &self.operator_taproot_public_key()) + ForceSkipConnector::new(self.network(), &self.n_of_n_taproot_public_key()) } pub fn kickoff_connector(&self) -> KickoffConnector { @@ -712,9 +714,7 @@ impl BitvmGcGraph { let operator_pre_sigs = if self.operator_pre_signed { Some(vec![ self.force_skip_kickoff.tx().input[0].witness.clone(), - self.force_skip_kickoff.tx().input[1].witness.clone(), self.quick_challenge.tx().input[0].witness.clone(), - self.quick_challenge.tx().input[1].witness.clone(), self.challenge_incomplete_kickoff.tx().input[0].witness.clone(), self.challenge_incomplete_kickoff.tx().input[1].witness.clone(), ]) @@ -747,6 +747,10 @@ impl BitvmGcGraph { disprove.push(extract_sig_from_witness(&disprove_tx.tx().input[0].witness)?); disprove.push(extract_sig_from_witness(&disprove_tx.tx().input[1].witness)?); } + let force_skip_kickoff = + vec![extract_sig_from_witness(&self.force_skip_kickoff.tx().input[1].witness)?]; + let quick_challenge = + vec![extract_sig_from_witness(&self.quick_challenge.tx().input[1].witness)?]; Some(CommitteeSignatures { take1, take2, @@ -755,6 +759,8 @@ impl BitvmGcGraph { operator_challenge_nack, operator_commit_timeout, disprove, + force_skip_kickoff, + quick_challenge, }) } else { None diff --git a/crates/bitvm-gc/src/verifier/api.rs b/crates/bitvm-gc/src/verifier/api.rs index 8cb87467d..20c01a873 100644 --- a/crates/bitvm-gc/src/verifier/api.rs +++ b/crates/bitvm-gc/src/verifier/api.rs @@ -79,6 +79,10 @@ pub fn build_force_skip_kickoff_tx( if !graph.operator_pre_signed() { bail!("missing pre-signatures from operator") }; + // Input 1 spends the force-skip connector, which only the committee signs. + if !graph.committee_pre_signed() { + bail!("missing pre-signatures from committee") + }; let mut tx = graph.force_skip_kickoff.tx().clone(); let total_input_amount = graph.force_skip_kickoff.prev_outs().iter().map(|o| o.value).sum(); let anchor_added = add_change_or_anchor_output( @@ -100,6 +104,10 @@ pub fn build_quick_challenge_tx( if !graph.operator_pre_signed() { bail!("missing pre-signatures from operator") }; + // Input 1 spends the force-skip connector, which only the committee signs. + if !graph.committee_pre_signed() { + bail!("missing pre-signatures from committee") + }; let mut tx = graph.quick_challenge.tx().clone(); let total_input_amount = graph.quick_challenge.prev_outs().iter().map(|o| o.value).sum(); let anchor_added = add_change_or_anchor_output( diff --git a/crates/bitvm-gc/tests/transaction_graph_invariants.rs b/crates/bitvm-gc/tests/transaction_graph_invariants.rs index 205a12149..144a428d3 100644 --- a/crates/bitvm-gc/tests/transaction_graph_invariants.rs +++ b/crates/bitvm-gc/tests/transaction_graph_invariants.rs @@ -196,6 +196,66 @@ fn csv_inputs_and_scripts_match_network_parameters() { } } +#[test] +fn committee_signs_both_spends_of_the_force_skip_connector() { + use bitcoin::{ + TapSighashType, XOnlyPublicKey, + key::Keypair, + secp256k1::{SECP256K1, SecretKey}, + }; + use bitvm_lib::{committee::*, connectors::base::TaprootConnector, operator::*}; + + let mut graph = generate_bitvm_graph(graph_parameters(Network::Regtest, 1, 1)).unwrap(); + let mut seed = [0u8; 32]; + seed[28..].copy_from_slice(&1u32.to_be_bytes()); + let keypair = Keypair::from_secret_key(SECP256K1, &SecretKey::from_slice(&seed).unwrap()); + let committee_pubkeys = graph.parameters.instance_parameters.committee_pubkeys.clone(); + + let force_skip = graph.parameters.force_skip_connector(); + assert_eq!(force_skip.n_of_n_taproot_public_key, graph.parameters.n_of_n_taproot_public_key()); + assert_eq!( + graph.next_prekickoff.tx().output[0].script_pubkey, + force_skip.generate_taproot_address().script_pubkey() + ); + + let witnesses = operator_pre_sign(keypair, &mut graph).unwrap(); + assert_eq!(witnesses.len(), operator_presig_num()); + verify_graph_operator_pre_signatures(&graph).unwrap(); + + let (pub_nonces, sec_nonces, _) = generate_nonce_from_seed("seed".into(), 0, keypair, 1, 1); + let agg_nonces = nonces_aggregation(std::slice::from_ref(&pub_nonces)).unwrap(); + let partial = committee_pre_sign(keypair, sec_nonces, agg_nonces.clone(), &mut graph).unwrap(); + verify_graph_committee_partial_sigs( + &graph, + &committee_pubkeys, + &committee_pubkeys[0], + &pub_nonces, + &agg_nonces, + &partial, + ) + .unwrap(); + let sigs = signature_aggregation(&vec![partial], &agg_nonces, &graph).unwrap(); + push_committee_pre_signatures(&mut graph, &sigs).unwrap(); + verify_graph_committee_pre_signatures(&graph).unwrap(); + + // The operator's key cannot spend the connector. + let operator = XOnlyPublicKey::from(graph.parameters.operator_pubkey); + for tx in [graph.quick_challenge.tx(), graph.force_skip_kickoff.tx()] { + assert_eq!(tx.input[1].witness.len(), 3); + } + assert!( + verify_taproot_pre_signed_input(&graph.quick_challenge, &operator, 1, TapSighashType::None) + .is_err() + ); + + let restored = + bitvm_lib::types::BitvmGcGraph::from_simplified(&graph.to_simplified().unwrap()).unwrap(); + verify_graph_operator_pre_signatures(&restored).unwrap(); + verify_graph_committee_pre_signatures(&restored).unwrap(); + assert_eq!(restored.quick_challenge.tx(), graph.quick_challenge.tx()); + assert_eq!(restored.force_skip_kickoff.tx(), graph.force_skip_kickoff.tx()); +} + fn graph_parameters( network: bitcoin::Network, watchtowers: usize, diff --git a/node/src/aggregates/graph_pegout_jobs.rs b/node/src/aggregates/graph_pegout_jobs.rs index 5bfef22de..b7a424799 100644 --- a/node/src/aggregates/graph_pegout_jobs.rs +++ b/node/src/aggregates/graph_pegout_jobs.rs @@ -523,6 +523,13 @@ async fn enforce_previous_slot(deps: &JobDeps, graph: &BitvmGcGraph) -> Result Result Result