diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs index 3c81528..d7270c3 100644 --- a/src/contract/outcome.rs +++ b/src/contract/outcome.rs @@ -5,7 +5,7 @@ use musig2::{ }; use secp::{Point, Scalar}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; use crate::{ contract::{ContractParameters, Outcome}, @@ -223,7 +223,7 @@ pub(crate) fn verify_outcome_tx_partial_signatures( pub(crate) struct OutcomeSignatures { /// A set of adaptor signatures which can be unlocked by the oracle's attestation /// for each outcome. - pub(crate) outcome_tx_signatures: Vec, + pub(crate) outcome_tx_signatures: BTreeMap, /// The complete signature on the expiry transaction. This is `None` if the /// [`ContractParameters::outcome_payouts`] field does not contain an @@ -253,7 +253,7 @@ where let funding_spend_info = &outcome_build_out.funding_spend_info; let mut signatures = OutcomeSignatures { - outcome_tx_signatures: Vec::with_capacity(params.event.outcome_messages.len()), + outcome_tx_signatures: BTreeMap::new(), expiry_tx_signature: None, }; @@ -282,7 +282,9 @@ where sighash, )?; - signatures.outcome_tx_signatures.push(adaptor_sig); + signatures + .outcome_tx_signatures + .insert(outcome_index, adaptor_sig); } Outcome::Expiry => { @@ -305,37 +307,43 @@ where /// outcome and expiry transactions. pub(crate) fn verify_outcome_tx_aggregated_signatures( params: &ContractParameters, + our_pubkey: Point, outcome_build_out: &OutcomeTransactionBuildOutput, - outcome_tx_signatures: &[AdaptorSignature], + outcome_tx_signatures: &BTreeMap, 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(); + // We only need to verify signatures on outcomes where our pubkey might + // win something. + let relevant_outcomes: BTreeSet = params + .win_conditions_controlled_by_pubkey(our_pubkey) + .ok_or(Error)? + .into_iter() + .map(|win_cond| win_cond.outcome) + .collect(); + // Construct a batch for efficient mass signature verification. - let batch: Vec = outcome_build_out - .outcome_txs - .iter() - .map(|(outcome, outcome_tx)| { + let batch: Vec = relevant_outcomes + .into_iter() + .map(|outcome| { + let outcome_tx = outcome_build_out.outcome_txs.get(&outcome).ok_or(Error)?; + 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) => { + 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)?; + let &signature = outcome_tx_signatures.get(&outcome_index).ok_or(Error)?; BatchVerificationRow::from_adaptor_signature( joint_pubkey, sighash, diff --git a/src/contract/split.rs b/src/contract/split.rs index aed7516..0ff72ab 100644 --- a/src/contract/split.rs +++ b/src/contract/split.rs @@ -13,7 +13,7 @@ use crate::{ spend_info::SplitSpendInfo, }; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; /// Represents the output of building the set of split transactions. /// This contains cached data used for constructing further transactions, @@ -274,18 +274,18 @@ where /// Verify the set of complete aggregated signatures on the split transactions. pub(crate) fn verify_split_tx_aggregated_signatures( params: &ContractParameters, + our_pubkey: Point, outcome_build_out: &OutcomeTransactionBuildOutput, split_build_out: &SplitTransactionBuildOutput, split_tx_signatures: &BTreeMap, ) -> Result<(), Error> { - let all_win_conditions = params.all_win_conditions(); + // We only need to verify signatures on win conditions where our pubkey might + // win something. + let relevant_win_conditions: BTreeSet = params + .win_conditions_controlled_by_pubkey(our_pubkey) + .ok_or(Error)?; - // One signature per win condition. - if split_tx_signatures.len() != all_win_conditions.len() { - return Err(Error); - } - - let batch: Vec = all_win_conditions + let batch: Vec = relevant_win_conditions .into_iter() .map(|win_cond| { let split_tx = split_build_out diff --git a/src/lib.rs b/src/lib.rs index d2fab27..cb6a918 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -21,7 +21,7 @@ use bitcoin::{OutPoint, Transaction, TxOut}; use musig2::{AdaptorSignature, AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; use secp::{MaybeScalar, Point, Scalar}; -use std::collections::BTreeMap; +use std::collections::{BTreeMap, BTreeSet}; pub use contract::{ContractParameters, Outcome, SigMap, WinCondition}; pub use oracles::EventAnnouncement; @@ -350,6 +350,7 @@ impl SigningSession { ) -> Result<(), Error> { contract::outcome::verify_outcome_tx_aggregated_signatures( &self.dlc.params, + self.our_public_key, &self.dlc.outcome_tx_build, &signatures.outcome_tx_signatures, signatures.expiry_tx_signature, @@ -357,6 +358,7 @@ impl SigningSession { contract::split::verify_split_tx_aggregated_signatures( &self.dlc.params, + self.our_public_key, &self.dlc.outcome_tx_build, &self.dlc.split_tx_build, &signatures.split_tx_signatures, @@ -395,17 +397,26 @@ fn validate_sigmaps_completeness( Ok(()) } +/// A set of signatures produced by running a cooperative [`SigningSession`] on a +/// [`TicketedDLC`]. These are only the signatures needed for enforcing outcomes +/// which multiple members of the group must agree on. +/// +/// Players do not need a fully copy of every outcome and split TX signature. +/// Only some players care about certain outcomes, and a player only enforce one +/// specific split TX unlock condition - the one corresponding to their ticket +/// hash. We can save bandwidth and #[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::outcome_messages`]. Each adaptor signature can be decrypted + /// A mapping of outcome attestation indexes to adaptor signatures on outcome transactions. + /// The index of each entry corresponds to the outcomes in + /// [`EventAnnouncement::outcome_messages`]. Each adaptor signature can be decrypted /// by the [`EventAnnouncement`]'s oracle producing an attestation signature using /// [`EventAnnouncement::attestation_secret`]. - pub outcome_tx_signatures: Vec, + pub outcome_tx_signatures: BTreeMap, /// 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, @@ -419,9 +430,59 @@ pub struct SignedContract { } impl SignedContract { - pub fn signatures(&self) -> &ContractSignatures { + /// Returns the complete set of signatures for all outcomes and win conditions. + pub fn all_signatures(&self) -> &ContractSignatures { &self.signatures } + + /// Produce a pruned set of signatures, relevant to the specific signer's pubkey. + /// If the pubkey belongs to the market maker, this simply returns a clone of the + /// full set of signatures, since the market maker is involved in every multisignature + /// spending condition. + pub fn pruned_signatures(&self, player_pubkey: Point) -> Option { + if player_pubkey == self.dlc.params.market_maker.pubkey { + return Some(self.signatures.clone()); + } + + let relevant_win_conditions = self + .dlc + .params + .win_conditions_controlled_by_pubkey(player_pubkey)?; + + let relevant_outcomes: BTreeSet = relevant_win_conditions + .iter() + .map(|win_cond| win_cond.outcome) + .collect(); + + let pruned_sigs = ContractSignatures { + expiry_tx_signature: self + .signatures + .expiry_tx_signature + .filter(|_| relevant_outcomes.contains(&Outcome::Expiry)), + + outcome_tx_signatures: self + .signatures + .outcome_tx_signatures + .iter() + .filter(|(&outcome_index, _)| { + relevant_outcomes.contains(&Outcome::Attestation(outcome_index)) + }) + .map(|(&outcome_index, &sig)| (outcome_index, sig)) + .collect(), + + split_tx_signatures: self + .signatures + .split_tx_signatures + .iter() + .filter(|(win_cond, _)| relevant_win_conditions.contains(win_cond)) + .map(|(&win_cond, &sig)| (win_cond, sig)) + .collect(), + }; + + Some(pruned_sigs) + } + + /// Returns the [`TicketedDLC`] which has been signed. pub fn dlc(&self) -> &TicketedDLC { &self.dlc } @@ -462,7 +523,7 @@ impl SignedContract { let adaptor_signature = self .signatures .outcome_tx_signatures - .get(outcome_index) + .get(&outcome_index) .ok_or(Error)?; let compact_sig: CompactSignature = adaptor_signature.adapt(attestation).ok_or(Error)?;