diff --git a/Cargo.lock b/Cargo.lock index 0d92bc6..336923e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -95,6 +95,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -149,6 +150,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3011d1213f159867b13cfd6ac92d2cd5f1345762c63be3554e84092d85a50bbd" +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + [[package]] name = "libc" version = "0.2.151" @@ -157,11 +167,12 @@ checksum = "302d7ab3130588088d277783b1e2d2e10c9e9e4a16dd9050e6ec93fb3e7048f4" [[package]] name = "musig2" -version = "0.0.4" +version = "0.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f43e7abc6e724a2c8caf0e558ab838d71e70826f98df85da6a607243efbc83f" +checksum = "d7453c75681e62712bcd3e3f9e630df0bc940037fdcded8d4839e3cc9bfc947a" dependencies = [ "base16ct", + "hmac", "once_cell", "rand", "secp", diff --git a/Cargo.toml b/Cargo.toml index dbe4608..2bd704b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dlc", "smart", "contract", "ticket", "auction"] [dependencies] bitcoin = { version = "0.31.1" } hex = "0.4.3" -musig2 = { version = "0.0.4", features = ["rand"] } +musig2 = { version = "0.0.7", features = ["rand"] } rand = "0.8.5" secp = { version = "0.2.1" } secp256k1 = { version = "0.28.2", features = ["global-context"] } @@ -20,3 +20,4 @@ sha2 = "0.10.8" [package.metadata.docs.rs] all-features = true +rustdoc-args = ["--cfg", "docsrs"] diff --git a/src/contract/mod.rs b/src/contract/mod.rs index 472dde5..2accec3 100644 --- a/src/contract/mod.rs +++ b/src/contract/mod.rs @@ -177,26 +177,34 @@ impl ContractParameters { Some(sigmap) } - /// Returns an empty sigmap covering every outcome and every win condition. - /// This encompasses every possible message whose signatures are needed - /// to set up the contract. - pub fn full_sigmap(&self) -> SigMap<()> { - let mut all_win_conditions = BTreeMap::new(); + /// Return a full set of all possible win conditions for this DLC. + pub fn all_win_conditions(&self) -> BTreeSet { + let mut all_win_conditions = BTreeSet::new(); for (&outcome, payout_map) in self.outcome_payouts.iter() { all_win_conditions.extend( payout_map .keys() - .map(|&winner| (WinCondition { winner, outcome }, ())), + .map(|&winner| WinCondition { winner, outcome }), ); } + all_win_conditions + } + /// Returns an empty sigmap covering every outcome and every win condition. + /// This encompasses every possible message whose signatures are needed + /// to set up the contract. + pub fn full_sigmap(&self) -> SigMap<()> { SigMap { by_outcome: self .outcome_payouts .iter() .map(|(&outcome, _)| (outcome, ())) .collect(), - by_win_condition: all_win_conditions, + by_win_condition: self + .all_win_conditions() + .into_iter() + .map(|win_cond| (win_cond, ())) + .collect(), } } } diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs index 397d0df..3c81528 100644 --- a/src/contract/outcome.rs +++ b/src/contract/outcome.rs @@ -1,13 +1,15 @@ use bitcoin::{absolute::LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut}; -use musig2::{AdaptorSignature, AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; -use secp::Scalar; +use musig2::{ + AdaptorSignature, AggNonce, BatchVerificationRow, CompactSignature, PartialSignature, PubNonce, + SecNonce, +}; +use secp::{Point, Scalar}; use std::collections::BTreeMap; use crate::{ contract::{ContractParameters, Outcome}, errors::Error, - parties::Player, spend_info::{FundingSpendInfo, OutcomeSpendInfo}, }; @@ -36,15 +38,6 @@ impl OutcomeTransactionBuildOutput { pub(crate) fn funding_spend_info(&self) -> &FundingSpendInfo { &self.funding_spend_info } - - /// Returns the number of [`musig2`] partial signatures required by each player - /// in the DLC (and the market maker). - /// - /// If the contract has an expiry payout condition, this is simply the number - /// of outcomes plus one. Otherwise it is the number of outcomes exactly. - pub(crate) fn signatures_required(&self) -> usize { - self.outcome_txs.len() - } } /// Construct a set of unsigned outcome transactions which spend from the funding TX. @@ -111,10 +104,7 @@ pub(crate) fn build_outcome_txs( } /// Construct a set of partial signatures for the outcome transactions. -/// -/// The number of signatures and nonces required can be computed by using -/// checking the length of [`OutcomeTransactionBuildOutput::signatures_required`]. -pub(crate) fn partial_sign_outcome_txs<'a>( +pub(crate) fn partial_sign_outcome_txs( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, seckey: Scalar, @@ -173,28 +163,21 @@ pub(crate) fn partial_sign_outcome_txs<'a>( } /// Verify a player's partial adaptor signatures on the outcome transactions. -/// -/// The number of signatures and nonces required can be computed by using -/// checking the length of [`OutcomeTransactionBuildOutput::signatures_required`]. -pub(crate) fn verify_outcome_tx_partial_signatures<'p, 'a>( +pub(crate) fn verify_outcome_tx_partial_signatures( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, - player: &Player, - pubnonces: impl IntoIterator, - aggnonces: impl IntoIterator, - partial_signatures: impl IntoIterator, + signer_pubkey: Point, + pubnonces: &BTreeMap, + aggnonces: &BTreeMap, + partial_signatures: &BTreeMap, ) -> Result<(), Error> { let outcome_txs = &outcome_build_out.outcome_txs; let funding_spend_info = &outcome_build_out.funding_spend_info; - let mut aggnonce_iter = aggnonces.into_iter(); - let mut pubnonce_iter = pubnonces.into_iter(); - let mut partial_sig_iter = partial_signatures.into_iter(); - for (&outcome, outcome_tx) in outcome_txs { - let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces - let pubnonce = pubnonce_iter.next().ok_or(Error)?; // must provide enough pubnonces - let partial_sig = partial_sig_iter.next().ok_or(Error)?; // must provide enough sigs + let aggnonce = aggnonces.get(&outcome).ok_or(Error)?; // must provide all aggnonces + let pubnonce = pubnonces.get(&outcome).ok_or(Error)?; // must provide all pubnonces + let &partial_sig = partial_signatures.get(&outcome).ok_or(Error)?; // must provide all sigs // Hash the outcome TX. let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?; @@ -212,7 +195,7 @@ pub(crate) fn verify_outcome_tx_partial_signatures<'p, 'a>( partial_sig, aggnonce, attestation_lock_point, - player.pubkey, + signer_pubkey, pubnonce, sighash, )?; @@ -223,7 +206,7 @@ pub(crate) fn verify_outcome_tx_partial_signatures<'p, 'a>( funding_spend_info.key_agg_ctx(), partial_sig, aggnonce, - player.pubkey, + signer_pubkey, pubnonce, sighash, )?; @@ -240,7 +223,7 @@ pub(crate) fn verify_outcome_tx_partial_signatures<'p, 'a>( pub(crate) struct OutcomeSignatures { /// A set of adaptor signatures which can be unlocked by the oracle's attestation /// for each outcome. - pub(crate) adaptor_signatures: Vec, + pub(crate) outcome_tx_signatures: Vec, /// The complete signature on the expiry transaction. This is `None` if the /// [`ContractParameters::outcome_payouts`] field does not contain an @@ -257,11 +240,11 @@ pub(crate) struct OutcomeSignatures { /// If all partial signatures are valid, then aggregation succeeds and this /// function outputs a set of adaptor signatures which are valid once adapted /// with the oracle's attestation. -pub(crate) fn aggregate_outcome_tx_adaptor_signatures<'a, S>( +pub(crate) fn aggregate_outcome_tx_adaptor_signatures( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, - aggnonces: impl IntoIterator, - partial_signature_groups: impl IntoIterator, + aggnonces: &BTreeMap, + mut partial_signature_groups: BTreeMap, ) -> Result where S: IntoIterator, @@ -269,19 +252,17 @@ where let outcome_txs = &outcome_build_out.outcome_txs; let funding_spend_info = &outcome_build_out.funding_spend_info; - let mut aggnonce_iter = aggnonces.into_iter(); - let mut partial_sig_group_iter = partial_signature_groups.into_iter(); - let mut signatures = OutcomeSignatures { - adaptor_signatures: Vec::with_capacity(params.event.outcome_messages.len()), + outcome_tx_signatures: Vec::with_capacity(params.event.outcome_messages.len()), expiry_tx_signature: None, }; for (&outcome, outcome_tx) in outcome_txs { // must provide a set of sigs for each TX - let partial_sigs = partial_sig_group_iter.next().ok_or(Error)?; + let partial_sigs = partial_signature_groups.remove(&outcome).ok_or(Error)?; - let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces + // must provide all aggnonces + let aggnonce = aggnonces.get(&outcome).ok_or(Error)?; // Hash the outcome TX. let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?; @@ -301,7 +282,7 @@ where sighash, )?; - signatures.adaptor_signatures.push(adaptor_sig); + signatures.outcome_tx_signatures.push(adaptor_sig); } Outcome::Expiry => { @@ -320,6 +301,66 @@ where Ok(signatures) } +/// Verify the set of complete aggregated signatures on the +/// outcome and expiry transactions. +pub(crate) fn verify_outcome_tx_aggregated_signatures( + params: &ContractParameters, + outcome_build_out: &OutcomeTransactionBuildOutput, + outcome_tx_signatures: &[AdaptorSignature], + expiry_tx_signature: Option, +) -> Result<(), Error> { + let funding_spend_info = &outcome_build_out.funding_spend_info; + + // One signature per outcome TX. + if outcome_tx_signatures.len() != params.event.outcome_messages.len() { + return Err(Error); + } + + let joint_pubkey: Point = funding_spend_info.key_agg_ctx().aggregated_pubkey(); + + // Construct a batch for efficient mass signature verification. + let batch: Vec = outcome_build_out + .outcome_txs + .iter() + .map(|(outcome, outcome_tx)| { + let sighash = outcome_build_out + .funding_spend_info + .sighash_tx_outcome(outcome_tx)?; + + let batch_row = match outcome { + // One adaptor signature for each possible attestation outcome. + &Outcome::Attestation(outcome_index) => { + let adaptor_point = params + .event + .attestation_lock_point(outcome_index) + .ok_or(Error)?; + + let &signature = outcome_tx_signatures.get(outcome_index).ok_or(Error)?; + BatchVerificationRow::from_adaptor_signature( + joint_pubkey, + sighash, + signature, + adaptor_point, + ) + } + + // One signature for the optional expiry transaction. + Outcome::Expiry => { + let signature = expiry_tx_signature.ok_or(Error)?.lift_nonce()?; + BatchVerificationRow::from_signature(joint_pubkey, sighash, signature) + } + }; + + Ok(batch_row) + }) + .collect::>()?; + + // Verify all outcome signatures at once. + musig2::verify_batch(&batch)?; + + Ok(()) +} + /// Construct an input to spend an outcome transaction for a specific outcome. /// Also returns a reference to the outcome TX's output so it can be used /// to construct a set of [`bitcoin::sighash::Prevouts`]. diff --git a/src/contract/split.rs b/src/contract/split.rs index bb3509b..9b5ce3a 100644 --- a/src/contract/split.rs +++ b/src/contract/split.rs @@ -1,6 +1,8 @@ use bitcoin::{absolute::LockTime, Amount, OutPoint, Sequence, Transaction, TxIn, TxOut}; -use musig2::{AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; -use secp::Scalar; +use musig2::{ + AggNonce, BatchVerificationRow, CompactSignature, PartialSignature, PubNonce, SecNonce, +}; +use secp::{Point, Scalar}; use crate::{ consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE}, @@ -11,7 +13,7 @@ use crate::{ spend_info::SplitSpendInfo, }; -use std::{borrow::Borrow, collections::BTreeMap}; +use std::collections::BTreeMap; /// Represents the output of building the set of split transactions. /// This contains cached data used for constructing further transactions, @@ -110,7 +112,7 @@ pub(crate) fn build_split_txs( /// /// The market maker must sign every split script spending path of every /// split transaction. -pub(crate) fn partial_sign_split_txs<'a>( +pub(crate) fn partial_sign_split_txs( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, split_build_out: &SplitTransactionBuildOutput, @@ -163,7 +165,7 @@ pub(crate) fn partial_sign_split_txs<'a>( Ok(partial_signatures) } -/// Verify the partial signatures provided by a signer across every split transaction. +/// Verify the partial signatures provided by a signer across every relevant split transaction. /// /// Since each split transaction may require multiple signatures (one for each [`WinCondition`]), /// the nonces and signatures must be provided in the form of a [`BTreeMap`], which maps @@ -173,13 +175,13 @@ pub(crate) fn verify_split_tx_partial_signatures( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, split_build_out: &SplitTransactionBuildOutput, - player: &Player, + signer_pubkey: Point, pubnonces: &BTreeMap, aggnonces: &BTreeMap, partial_signatures: &BTreeMap, ) -> Result<(), Error> { let win_conditions_to_sign = params - .win_conditions_controlled_by_pubkey(player.pubkey) + .win_conditions_controlled_by_pubkey(signer_pubkey) .ok_or(Error)?; for win_cond in win_conditions_to_sign { @@ -205,7 +207,7 @@ pub(crate) fn verify_split_tx_partial_signatures( outcome_spend_info.key_agg_ctx_untweaked(), partial_sig, aggnonce, - player.pubkey, + signer_pubkey, pubnonce, sighash, )?; @@ -220,15 +222,14 @@ pub(crate) fn verify_split_tx_partial_signatures( /// the nonces and signatures must be provided in the form of a [`BTreeMap`], which maps /// a [`WinCondition`] to the appropriate signature for that script spending path. /// If any signatures or nonces are missing, this method returns an error. -pub(crate) fn aggregate_split_tx_signatures<'s, S, P>( +pub(crate) fn aggregate_split_tx_signatures( outcome_build_out: &OutcomeTransactionBuildOutput, split_build_out: &SplitTransactionBuildOutput, aggnonces: &BTreeMap, - partial_signatures_by_win_cond: &'s BTreeMap, + mut partial_signatures_by_win_cond: BTreeMap, ) -> Result, Error> where - &'s S: IntoIterator, - P: Borrow, + S: IntoIterator, { split_build_out .split_spend_infos @@ -240,10 +241,8 @@ where .ok_or(Error)?; let relevant_partial_sigs = partial_signatures_by_win_cond - .get(&win_cond) - .ok_or(Error)? - .into_iter() - .map(|sig| sig.borrow().clone()); + .remove(&win_cond) + .ok_or(Error)?; let aggnonce = aggnonces.get(&win_cond).ok_or(Error)?; @@ -267,6 +266,58 @@ where .collect() } +/// Verify the set of complete aggregated signatures on the split transactions. +pub(crate) fn verify_split_tx_aggregated_signatures( + params: &ContractParameters, + outcome_build_out: &OutcomeTransactionBuildOutput, + split_build_out: &SplitTransactionBuildOutput, + split_tx_signatures: &BTreeMap, +) -> Result<(), Error> { + let all_win_conditions = params.all_win_conditions(); + + // One signature per win condition. + if split_tx_signatures.len() != all_win_conditions.len() { + return Err(Error); + } + + let batch: Vec = all_win_conditions + .into_iter() + .map(|win_cond| { + let split_tx = split_build_out + .split_txs() + .get(&win_cond.outcome) + .ok_or(Error)?; + + let signature = split_tx_signatures.get(&win_cond).ok_or(Error)?; + + let outcome_spend_info = outcome_build_out + .outcome_spend_infos() + .get(&win_cond.outcome) + .ok_or(Error)?; + + // Expect an untweaked signature by the group, so that the signature + // can be used to trigger one of the players' split tapscripts. + let winners_joint_pubkey: Point = outcome_spend_info + .key_agg_ctx_untweaked() + .aggregated_pubkey(); + + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + + let batch_row = BatchVerificationRow::from_signature( + winners_joint_pubkey, + sighash, + signature.lift_nonce()?, + ); + + Ok(batch_row) + }) + .collect::>()?; + + musig2::verify_batch(&batch)?; + + Ok(()) +} + /// Construct an input to spend a given player's output of the split transaction /// for a specific outcome. Also returns a reference to the split TX's output so /// it can be used to construct a set of [`bitcoin::sighash::Prevouts`]. diff --git a/src/errors.rs b/src/errors.rs index 58d21ca..1806f95 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -34,6 +34,12 @@ impl From for Error { } } +impl From for Error { + fn from(_: secp::errors::InvalidPointBytes) -> Self { + Error + } +} + impl From for Error { fn from(_: bitcoin::taproot::TaprootBuilderError) -> Self { Error diff --git a/src/lib.rs b/src/lib.rs index 21ff2fc..cde857d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -13,15 +13,15 @@ pub use secp; pub use parties::{MarketMaker, Player}; -use contract::outcome::{ - build_outcome_txs, partial_sign_outcome_txs, OutcomeTransactionBuildOutput, +use contract::{ + outcome::{OutcomeSignatures, OutcomeTransactionBuildOutput}, + split::SplitTransactionBuildOutput, + ContractParameters, SigMap, WinCondition, }; -use contract::split::{build_split_txs, partial_sign_split_txs, SplitTransactionBuildOutput}; -use contract::{ContractParameters, SigMap}; use errors::Error; use bitcoin::{OutPoint, TxOut}; -use musig2::{AggNonce, PartialSignature, PubNonce, SecNonce}; +use musig2::{AdaptorSignature, AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; use secp::{Point, Scalar}; use std::collections::BTreeMap; @@ -40,8 +40,8 @@ impl TicketedDLC { params: ContractParameters, funding_outpoint: OutPoint, ) -> Result { - let outcome_tx_build = build_outcome_txs(¶ms, funding_outpoint)?; - let split_tx_build = build_split_txs(¶ms, &outcome_tx_build)?; + let outcome_tx_build = contract::outcome::build_outcome_txs(¶ms, funding_outpoint)?; + let split_tx_build = contract::split::build_split_txs(¶ms, &outcome_tx_build)?; let txs = TicketedDLC { params, @@ -85,12 +85,37 @@ pub struct PartialSignatureSharingRound { our_partial_signatures: SigMap, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ContractSignatures { + /// A complete signature on the expiry transaction. Set to `None` if the + /// [`ContractParameters::outcome_payouts`] field did not contain an + /// [`Outcome::Expiry`] payout condition. + pub expiry_tx_signature: Option, + /// An ordered vector of adaptor signatures, corresponding to each of the outcomes + /// in [`EventAnnouncement::outcomes`]. Each adaptor signature can be decrypted + /// by the [`EventAnnouncement`]'s oracle producing an attestation signature using + /// [`EventAnnouncement::attestation_secret`]. + pub outcome_tx_signatures: Vec, + /// A set of signatures needed for broadcasting split transactions. Each signature + /// is specific to a certain combination of player and outcome. + pub split_tx_signatures: BTreeMap, +} + +/// A [`SigningSessionState`] used for a complete signing session once +/// all signatures on the [`TicketedDLC`] have been aggregated and verified +/// successfully. +pub struct CompleteState { + signatures: ContractSignatures, +} + impl SigningSessionState for NonceSharingRound {} impl SigningSessionState for PartialSignatureSharingRound {} +impl SigningSessionState for CompleteState {} /// This is a state machine to manage signing the various transactions in a [`TicketedDLC`]. pub struct SigningSession { dlc: TicketedDLC, + our_public_key: Point, state: S, } @@ -99,20 +124,24 @@ impl SigningSession { pub fn dlc(&self) -> &TicketedDLC { &self.dlc } + + /// Return the public key of our signer. + pub fn our_public_key(&self) -> Point { + self.our_public_key + } } impl SigningSession { + /// Start a new signing session on the given TicketedDLC. pub fn new( dlc: TicketedDLC, mut rng: &mut R, signing_key: impl Into, ) -> Result, Error> { let signing_key = signing_key.into(); + let our_public_key = signing_key.base_point_mul(); - let base_sigmap = dlc - .params - .sigmap_for_pubkey(signing_key.base_point_mul()) - .ok_or(Error)?; + let base_sigmap = dlc.params.sigmap_for_pubkey(our_public_key).ok_or(Error)?; let our_secret_nonces = base_sigmap.map_values(|_| { SecNonce::build(&mut rng) @@ -127,6 +156,7 @@ impl SigningSession { let session = SigningSession { dlc, + our_public_key, state: NonceSharingRound { signing_key: signing_key.into(), our_secret_nonces, @@ -136,45 +166,22 @@ impl SigningSession { Ok(session) } - /// The public nonces we should send to other signers. + /// The public nonces we should broadcast to other signers. pub fn our_public_nonces(&self) -> &SigMap { &self.state.our_public_nonces } - /// Receive the nonces from all other signers. - pub fn compute_all_signatures( + /// Receive the nonces from all other signers and construct our set of partial + /// signatures. This begins the `PartialSignatureSharingRound` of the `SigningSession`. + pub fn compute_partial_signatures( self, mut received_nonces: BTreeMap>, ) -> Result, Error> { // Insert our own public nonces so that callers don't need // to inject them manually. - received_nonces.insert( - self.state.signing_key.base_point_mul(), - self.state.our_public_nonces, - ); + received_nonces.insert(self.our_public_key, self.state.our_public_nonces); - // Must receive nonces from all players and the market maker. - if !received_nonces.contains_key(&self.dlc.params.market_maker.pubkey) { - return Err(Error); - } - for player in self.dlc.params.players.iter() { - if !received_nonces.contains_key(&player.pubkey) { - return Err(Error); - } - } - - // The expected sigmaps each signer must provide nonces for. - let base_sigmaps: BTreeMap> = received_nonces - .keys() - .map(|&key| Ok((key, self.dlc.params.sigmap_for_pubkey(key).ok_or(Error)?))) - .collect::>()?; - - for (&signer_pubkey, nonces) in received_nonces.iter() { - // All signers' sigmaps must match exactly. - if !nonces.is_mirror(&base_sigmaps[&signer_pubkey]) { - return Err(Error); - } - } + validate_sigmaps_completeness(&self.dlc.params, &received_nonces)?; let aggregated_nonces: SigMap = self.dlc.params.full_sigmap().map( |outcome, _| { @@ -192,14 +199,14 @@ impl SigningSession { ); let our_partial_signatures = SigMap { - by_outcome: partial_sign_outcome_txs( + by_outcome: contract::outcome::partial_sign_outcome_txs( &self.dlc.params, &self.dlc.outcome_tx_build, self.state.signing_key, self.state.our_secret_nonces.by_outcome, &aggregated_nonces.by_outcome, )?, - by_win_condition: partial_sign_split_txs( + by_win_condition: contract::split::partial_sign_split_txs( &self.dlc.params, &self.dlc.outcome_tx_build, &self.dlc.split_tx_build, @@ -211,6 +218,7 @@ impl SigningSession { let session = SigningSession { dlc: self.dlc, + our_public_key: self.our_public_key, state: PartialSignatureSharingRound { received_nonces, aggregated_nonces, @@ -220,3 +228,186 @@ impl SigningSession { Ok(session) } } + +impl SigningSession { + /// Returns the set of partial signatures which should be shared + /// with our signing peers. + pub fn our_partial_signatures(&self) -> &SigMap { + &self.state.our_partial_signatures + } + + /// Returns the set of aggregated nonces which should be sent to + /// our signing peers if they don't already have them. + pub fn aggregated_nonces(&self) -> &SigMap { + &self.state.aggregated_nonces + } + + /// Verify the signatures received from a particular signer. Returns an + /// error if any signatures are missing, or if any signatures are not + /// correct. + pub fn verify_partial_signatures( + &self, + signer_pubkey: Point, + partial_signatures: &SigMap, + ) -> Result<(), Error> { + let signer_nonces = self + .state + .received_nonces + .get(&signer_pubkey) + .ok_or(Error)?; + + contract::outcome::verify_outcome_tx_partial_signatures( + &self.dlc.params, + &self.dlc.outcome_tx_build, + signer_pubkey, + &signer_nonces.by_outcome, + &self.state.aggregated_nonces.by_outcome, + &partial_signatures.by_outcome, + )?; + + contract::split::verify_split_tx_partial_signatures( + &self.dlc.params, + &self.dlc.outcome_tx_build, + &self.dlc.split_tx_build, + signer_pubkey, + &signer_nonces.by_win_condition, + &self.state.aggregated_nonces.by_win_condition, + &partial_signatures.by_win_condition, + )?; + + Ok(()) + } + + /// Combine all the partial signatures received from peers. Assumes all signature sets have + /// been verified individually using [`verify_partial_signatures`][Self::verify_partial_signatures]. + /// The aggregated signatures will still be verified before this method returns, but not in + /// a way that blame can be properly placed on erroneous peer signatures. + /// + /// This completes the signing session. + pub fn aggregate_all_signatures( + self, + mut received_signatures: BTreeMap>, + ) -> Result, Error> { + // Insert our own signatures so that callers don't need to inject them manually. + received_signatures.insert(self.our_public_key, self.state.our_partial_signatures); + + validate_sigmaps_completeness(&self.dlc.params, &received_signatures)?; + + let full_sigmap = self.dlc.params.full_sigmap(); + + // We were given a map of the partial signatures made by each signer. We + // restructure them to map outcomes and win conditions into the full set of + // partial signatures which should be aggregated per sighash. + // + // For each sighash we need to sign... + let partial_signature_sets: SigMap> = full_sigmap.map( + // Collect all the partial signatures needed for each outcome + |outcome, _| { + received_signatures + .values() + .filter_map(|sig_sigmap| sig_sigmap.by_outcome.get(&outcome).copied()) + .collect::>() + }, + // Collect all the partial signatures needed for each win condition + |win_cond, _| { + received_signatures + .values() + .filter_map(|sig_sigmap| sig_sigmap.by_win_condition.get(&win_cond).copied()) + .collect::>() + }, + ); + + // Aggregate all the outcome TX signatures. + let OutcomeSignatures { + outcome_tx_signatures, + expiry_tx_signature, + } = contract::outcome::aggregate_outcome_tx_adaptor_signatures( + &self.dlc.params, + &self.dlc.outcome_tx_build, + &self.state.aggregated_nonces.by_outcome, + partial_signature_sets.by_outcome, + )?; + + // Aggregate all the split TX signatures. + let split_tx_signatures = contract::split::aggregate_split_tx_signatures( + &self.dlc.outcome_tx_build, + &self.dlc.split_tx_build, + &self.state.aggregated_nonces.by_win_condition, + partial_signature_sets.by_win_condition, + )?; + + // Signing complete! Just have to send `signatures` to our peers. + let complete_session = SigningSession { + dlc: self.dlc, + our_public_key: self.our_public_key, + state: CompleteState { + signatures: ContractSignatures { + expiry_tx_signature, + outcome_tx_signatures, + split_tx_signatures, + }, + }, + }; + + Ok(complete_session) + } + + /// Verifies the complete set of contract signatures were aggregated correctly by a peer. + /// + /// This completes the signing session. + pub fn verify_aggregated_signatures( + &self, + signatures: &ContractSignatures, + ) -> Result<(), Error> { + contract::outcome::verify_outcome_tx_aggregated_signatures( + &self.dlc.params, + &self.dlc.outcome_tx_build, + &signatures.outcome_tx_signatures, + signatures.expiry_tx_signature, + )?; + + contract::split::verify_split_tx_aggregated_signatures( + &self.dlc.params, + &self.dlc.outcome_tx_build, + &self.dlc.split_tx_build, + &signatures.split_tx_signatures, + )?; + + Ok(()) + } +} + +/// This validates a set of sigmaps received from untrusted peers. Ensures +/// each sigmap contains a full set, matching the expected sigmap for the sender. +fn validate_sigmaps_completeness( + params: &ContractParameters, + received_maps: &BTreeMap>, +) -> Result<(), Error> { + // Must receive signatures/nonces from all players and the market maker. + if !received_maps.contains_key(¶ms.market_maker.pubkey) { + return Err(Error); + } + for player in params.players.iter() { + if !received_maps.contains_key(&player.pubkey) { + return Err(Error); + } + } + + for (&signer_pubkey, sigmap) in received_maps.iter() { + // The expected sigmap each signer must provide nonces/signatures for. + let base_sigmap = params.sigmap_for_pubkey(signer_pubkey).ok_or(Error)?; + + // All signers' sigmaps must match exactly. + if !sigmap.is_mirror(&base_sigmap) { + return Err(Error); + } + } + + Ok(()) +} + +impl SigningSession { + pub fn signatures(&self) -> &ContractSignatures { + &self.state.signatures + } +}