From c24f6cc2fb99d61f38b082202c899cf368abdae6 Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 9 Nov 2026 07:45:03 +0000 Subject: [PATCH 1/2] lock force-skip connector to the committee --- goat/src/connectors/kickoff_connectors.rs | 13 +- goat/src/transactions/prekickoff.rs | 207 +++++++++++++++++++--- 2 files changed, 189 insertions(+), 31 deletions(-) diff --git a/goat/src/connectors/kickoff_connectors.rs b/goat/src/connectors/kickoff_connectors.rs index dafd9ba4..1efc2ab4 100644 --- a/goat/src/connectors/kickoff_connectors.rs +++ b/goat/src/connectors/kickoff_connectors.rs @@ -172,22 +172,25 @@ impl TaprootConnector for KickoffConnector { } } +/// Output 0 of the next Pre-Kickoff, spent only by the committee-signed +/// Quick-Challenge or Force-Skip-Kickoff. The operator holds no key to it, so +/// it cannot sweep the output and make those penalties unspendable. #[derive(Serialize, Deserialize, Eq, PartialEq, Clone)] pub struct ForceSkipConnector { pub network: Network, - pub operator_taproot_public_key: XOnlyPublicKey, + pub n_of_n_taproot_public_key: XOnlyPublicKey, } impl ForceSkipConnector { - pub fn new(network: Network, operator_taproot_public_key: &XOnlyPublicKey) -> Self { + pub fn new(network: Network, n_of_n_taproot_public_key: &XOnlyPublicKey) -> Self { ForceSkipConnector { network, - operator_taproot_public_key: *operator_taproot_public_key, + n_of_n_taproot_public_key: *n_of_n_taproot_public_key, } } fn generate_taproot_leaf_0_script(&self) -> ScriptBuf { - generate_pay_to_pubkey_taproot_script(&self.operator_taproot_public_key) + generate_pay_to_pubkey_taproot_script(&self.n_of_n_taproot_public_key) } fn generate_taproot_leaf_0_tx_in(&self, input: &Input) -> TxIn { @@ -214,7 +217,7 @@ impl TaprootConnector for ForceSkipConnector { TaprootBuilder::new() .add_leaf(0, self.generate_taproot_leaf_0_script()) .expect("Unable to add leaf 0") - .finalize(SECP256K1, self.operator_taproot_public_key) + .finalize(SECP256K1, self.n_of_n_taproot_public_key) .expect("Unable to finalize taproot") } diff --git a/goat/src/transactions/prekickoff.rs b/goat/src/transactions/prekickoff.rs index 5f0a5dea..8d915f2b 100644 --- a/goat/src/transactions/prekickoff.rs +++ b/goat/src/transactions/prekickoff.rs @@ -1,6 +1,7 @@ use bitcoin::{ absolute, consensus, Address, Amount, ScriptBuf, TapSighashType, Transaction, TxIn, TxOut, }; +use musig2::{errors::SigningError, AggNonce, PartialSignature, SecNonce}; use serde::{Deserialize, Serialize}; use crate::{ @@ -10,10 +11,18 @@ use crate::{ ForceSkipConnector, GuardianConnector, KickoffConnector, PrekickoffConnector, }, }, - contexts::operator::OperatorContext, + contexts::{base::BaseContext, committee::CommitteeContext, operator::OperatorContext}, error::{Error, TransactionError::InsufficientInputAmount}, scripts::p2a_output, - transactions::signing::populate_taproot_input_witness_default, + transactions::{ + signing::{ + populate_taproot_input_witness_default, + push_taproot_leaf_script_and_control_block_to_witness, + }, + signing_musig2::{ + generate_taproot_aggregated_signature, generate_taproot_partial_signature, + }, + }, }; use super::{base::*, pre_signed::*}; @@ -266,7 +275,7 @@ impl ForceSkipKickoffTransaction { input_1, ); - this.pre_sign_and_push(context, kickoff_connector, next_force_skip_connector); + this.pre_sign_and_push(context, kickoff_connector); this } @@ -325,29 +334,102 @@ impl ForceSkipKickoffTransaction { ); } - pub fn sign_input_1( + fn sign_input_1_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 1; + let sighash_type = TapSighashType::None; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_1_signature( &mut self, - context: &OperatorContext, next_force_skip_connector: &ForceSkipConnector, + input_1_sig: bitcoin::taproot::Signature, ) { let input_index = 1; - pre_sign_taproot_input_default( - self, + let script = self.prev_scripts()[input_index].clone(); + let spend_info = next_force_skip_connector.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + tx_mut.input[input_index] + .witness + .push(input_1_sig.serialize()); + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, input_index, - TapSighashType::None, - next_force_skip_connector.generate_taproot_spend_info(), - &vec![&context.operator_keypair], + &spend_info, + &script, ); } - pub fn pre_sign_and_push( + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[PartialSignature; 1], SigningError> { + let input_1_sig = self.sign_input_1_musig2(context, &sec_nonces[0], &agg_nonces[0]); + match [input_1_sig] + .into_iter() + .collect::, SigningError>>() + { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[bitcoin::taproot::Signature; 1], Error> { + let input_index = 1; + let sig_index = 0; + let sighash_type = TapSighashType::None; + let input_1_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { + signature: sig.into(), + sighash_type, + }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + Ok([input_1_sig]) + } + + pub fn push_pre_sigs( &mut self, - context: &OperatorContext, - kickoff_connector: &KickoffConnector, next_force_skip_connector: &ForceSkipConnector, + pre_sigs: [bitcoin::taproot::Signature; 1], ) { + self.push_input_1_signature(next_force_skip_connector, pre_sigs[0]); + } + + /// Signs input 0. Input 1 spends the force-skip connector and is signed + /// by the committee through `pre_sign`. + pub fn pre_sign_and_push(&mut self, context: &OperatorContext, kickoff_connector: &KickoffConnector) { self.sign_input_0(context, kickoff_connector); - self.sign_input_1(context, next_force_skip_connector); } } impl BaseTransaction for ForceSkipKickoffTransaction { @@ -399,7 +481,7 @@ impl QuickChallengeTransaction { input_1, ); - this.pre_sign_and_push(context, guardian_connector, next_force_skip_connector); + this.pre_sign_and_push(context, guardian_connector); this } @@ -460,29 +542,102 @@ impl QuickChallengeTransaction { ); } - pub fn sign_input_1( + fn sign_input_1_musig2( + &mut self, + context: &CommitteeContext, + sec_nonce: &SecNonce, + agg_nonce: &AggNonce, + ) -> Result { + let input_index = 1; + let sighash_type = TapSighashType::None; + generate_taproot_partial_signature( + context, + self.tx(), + sec_nonce, + agg_nonce, + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + ) + } + + fn push_input_1_signature( &mut self, - context: &OperatorContext, next_force_skip_connector: &ForceSkipConnector, + input_1_sig: bitcoin::taproot::Signature, ) { let input_index = 1; - pre_sign_taproot_input_default( - self, + let script = self.prev_scripts()[input_index].clone(); + let spend_info = next_force_skip_connector.generate_taproot_spend_info(); + let tx_mut = self.tx_mut(); + tx_mut.input[input_index] + .witness + .push(input_1_sig.serialize()); + push_taproot_leaf_script_and_control_block_to_witness( + tx_mut, input_index, - TapSighashType::None, - next_force_skip_connector.generate_taproot_spend_info(), - &vec![&context.operator_keypair], + &spend_info, + &script, ); } - pub fn pre_sign_and_push( + pub fn pre_sign( + &mut self, + context: &CommitteeContext, + sec_nonces: &[SecNonce; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[PartialSignature; 1], SigningError> { + let input_1_sig = self.sign_input_1_musig2(context, &sec_nonces[0], &agg_nonces[0]); + match [input_1_sig] + .into_iter() + .collect::, SigningError>>() + { + Ok(sigs) => Ok(sigs.try_into().unwrap()), + Err(e) => Err(e), + } + } + + pub fn aggregate_pre_sigs( + &self, + context: &dyn BaseContext, + partial_signatures: &[Vec; 1], + agg_nonces: &[AggNonce; 1], + ) -> Result<[bitcoin::taproot::Signature; 1], Error> { + let input_index = 1; + let sig_index = 0; + let sighash_type = TapSighashType::None; + let input_1_sig = match generate_taproot_aggregated_signature( + context, + self.tx(), + &agg_nonces[sig_index], + input_index, + self.prev_outs(), + &self.prev_scripts()[input_index], + sighash_type, + partial_signatures[sig_index].clone(), + ) { + Ok(sig) => bitcoin::taproot::Signature { + signature: sig.into(), + sighash_type, + }, + Err(_) => return Err(Error::Other("Failed to aggregate signatures")), + }; + Ok([input_1_sig]) + } + + pub fn push_pre_sigs( &mut self, - context: &OperatorContext, - guardian_connector: &GuardianConnector, next_force_skip_connector: &ForceSkipConnector, + pre_sigs: [bitcoin::taproot::Signature; 1], ) { + self.push_input_1_signature(next_force_skip_connector, pre_sigs[0]); + } + + /// Signs input 0. Input 1 spends the force-skip connector and is signed + /// by the committee through `pre_sign`. + pub fn pre_sign_and_push(&mut self, context: &OperatorContext, guardian_connector: &GuardianConnector) { self.sign_input_0(context, guardian_connector); - self.sign_input_1(context, next_force_skip_connector); } } impl BaseTransaction for QuickChallengeTransaction { From 2c76a2c3df19026a4d514665bd30da7615a8990b Mon Sep 17 00:00:00 2001 From: Ethan Date: Mon, 9 Nov 2026 10:59:40 +0000 Subject: [PATCH 2/2] format prekickoff --- goat/src/transactions/prekickoff.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/goat/src/transactions/prekickoff.rs b/goat/src/transactions/prekickoff.rs index 8d915f2b..2ade618f 100644 --- a/goat/src/transactions/prekickoff.rs +++ b/goat/src/transactions/prekickoff.rs @@ -428,7 +428,11 @@ impl ForceSkipKickoffTransaction { /// Signs input 0. Input 1 spends the force-skip connector and is signed /// by the committee through `pre_sign`. - pub fn pre_sign_and_push(&mut self, context: &OperatorContext, kickoff_connector: &KickoffConnector) { + pub fn pre_sign_and_push( + &mut self, + context: &OperatorContext, + kickoff_connector: &KickoffConnector, + ) { self.sign_input_0(context, kickoff_connector); } } @@ -636,7 +640,11 @@ impl QuickChallengeTransaction { /// Signs input 0. Input 1 spends the force-skip connector and is signed /// by the committee through `pre_sign`. - pub fn pre_sign_and_push(&mut self, context: &OperatorContext, guardian_connector: &GuardianConnector) { + pub fn pre_sign_and_push( + &mut self, + context: &OperatorContext, + guardian_connector: &GuardianConnector, + ) { self.sign_input_0(context, guardian_connector); } }