From cd681d087463599425cec548b171f612744b22a9 Mon Sep 17 00:00:00 2001 From: conduition Date: Wed, 21 Feb 2024 06:13:31 +0000 Subject: [PATCH] add expiry outcome branch --- src/contract/mod.rs | 64 +++++++---- src/contract/outcome.rs | 239 +++++++++++++++++++++++++--------------- src/contract/split.rs | 66 +++++------ 3 files changed, 229 insertions(+), 140 deletions(-) diff --git a/src/contract/mod.rs b/src/contract/mod.rs index ff9f610..ee14271 100644 --- a/src/contract/mod.rs +++ b/src/contract/mod.rs @@ -45,15 +45,13 @@ pub struct ContractParameters { /// The event whose outcome determines the payouts. pub event: EventAnnouncment, - /// An ordered list of payout under different outcomes. Should align with - /// `self.event.outcome_messages`. - pub outcome_payouts: Vec, - - /// Who is paid out in the event of an expiry (when the oracle attestation is not - /// received by [`event.expiry`][EventAnnouncment::expiry]). If this field is `None`, - /// then there is no expiry condition, and the money simply remains locked in the - /// funding outpoint until the Oracle's attestation is found. - pub expiry_payout: Option, + /// A mapping of payout weights under different outcomes. Attestation indexes should + /// align with [`self.event.outcome_messages`][EventAnnouncment::outcome_messages]. + /// + /// If this map does not contain a key of [`Outcome::Expiry`], then there is no expiry + /// condition, and the money simply remains locked in the funding outpoint until the + /// Oracle's attestation is found. + pub outcome_payouts: BTreeMap, /// A default mining fee rate to be used for pre-signed transactions. pub fee_rate: FeeRate, @@ -74,10 +72,22 @@ pub struct ContractParameters { pub relative_locktime_block_delta: u16, } +/// Represents one possible outcome branch of the DLC. This includes both +/// outcomes attested-to by the Oracle, and expiry. +#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub enum Outcome { + /// Indicates the oracle attested to a particular outcome of the given index. + Attestation(usize), + + /// Indicates the oracle failed to attest to any outcome, and the event expiry + /// timelock was reached. + Expiry, +} + /// Points to a situation where a player wins a payout from the DLC. #[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct WinCondition { - pub outcome_index: usize, + pub outcome: Outcome, pub winner: Player, } @@ -89,12 +99,29 @@ impl ContractParameters { Ok(outcome_value) } + /// Returns the set of players which this pubkey can sign for. + /// + /// This might contain multiple players if the same key joined the DLC + /// with different ticket/payout hashes. + pub fn players_controlled_by_pubkey<'a>(&'a self, pubkey: Point) -> BTreeSet<&'a Player> { + self.players + .iter() + .filter(|player| player.pubkey == pubkey) + .collect() + } + /// Return the set of all win conditions which the given pubkey will need to sign /// split transactions for. /// + /// If `pubkey` belongs to one or more players, this returns all [`WinCondition`]s + /// for outcomes in which the player or players are winners. + /// + /// If `pubkey` belongs to the market maker, this returns every [`WinCondition`] + /// across the entire contract. + /// /// Returns `None` if the pubkey does not belong to any player in the DLC. /// - /// Returns an empty `BTreeSet if the player is part of the DLC, but isn't due to + /// Returns an empty `BTreeSet` if the player is part of the DLC, but isn't due to /// receive any payouts on any DLC outcome. pub fn win_conditions_controlled_by_pubkey( &self, @@ -103,13 +130,7 @@ impl ContractParameters { // To sign as the market maker, the caller need only provide the correct secret key. let is_market_maker = pubkey == self.market_maker.pubkey; - // This might contain multiple players if the same key joined the DLC - // with different ticket/payout hashes. - let controlling_players: BTreeSet<&Player> = self - .players - .iter() - .filter(|player| player.pubkey == pubkey) - .collect(); + let controlling_players = self.players_controlled_by_pubkey(pubkey); // Short circuit if this pubkey is not known. if controlling_players.is_empty() && !is_market_maker { @@ -117,17 +138,14 @@ impl ContractParameters { } let mut win_conditions_to_sign = BTreeSet::::new(); - for (outcome_index, payout_map) in self.outcome_payouts.iter().enumerate() { + for (&outcome, payout_map) in self.outcome_payouts.iter() { // We want to sign the split TX for any win-conditions whose player is controlled // by `pubkey`. If we're the market maker, we sign every win condition. win_conditions_to_sign.extend( payout_map .keys() .filter(|winner| is_market_maker || controlling_players.contains(winner)) - .map(|&winner| WinCondition { - winner, - outcome_index, - }), + .map(|&winner| WinCondition { winner, outcome }), ); } diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs index c0967fe..1415a80 100644 --- a/src/contract/outcome.rs +++ b/src/contract/outcome.rs @@ -1,9 +1,11 @@ use bitcoin::{absolute::LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut}; -use musig2::{AdaptorSignature, AggNonce, PartialSignature, PubNonce, SecNonce}; +use musig2::{AdaptorSignature, AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; use secp::Scalar; +use std::collections::BTreeMap; + use crate::{ - contract::ContractParameters, + contract::{ContractParameters, Outcome}, errors::Error, parties::Player, spend_info::{FundingSpendInfo, OutcomeSpendInfo}, @@ -13,26 +15,28 @@ use crate::{ /// This contains cached data used for constructing further transactions, /// or signing the outcome transactions themselves. pub(crate) struct OutcomeTransactionBuildOutput { - outcome_txs: Vec, - outcome_spend_infos: Vec, + outcome_txs: BTreeMap, + outcome_spend_infos: BTreeMap, funding_spend_info: FundingSpendInfo, } impl OutcomeTransactionBuildOutput { /// Return the set of mutually exclusive outcome transactions. One of these /// transactions will be executed depending on the oracle's attestation. - pub(crate) fn outcome_txs(&self) -> &[Transaction] { + pub(crate) fn outcome_txs(&self) -> &BTreeMap { &self.outcome_txs } /// Return the set of mutually exclusive outcome spend info objects. - pub(crate) fn outcome_spend_infos(&self) -> &[OutcomeSpendInfo] { + pub(crate) fn outcome_spend_infos(&self) -> &BTreeMap { &self.outcome_spend_infos } /// Returns the number of [`musig2`] partial signatures required by each player - /// in the DLC (and the market maker). This is the same as the length of - /// [`Self::outcome_txs`]. + /// 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() } @@ -50,34 +54,42 @@ pub(crate) fn build_outcome_txs( }; let outcome_value = params.outcome_output_value()?; - let n_outcomes = params.event.outcome_messages.len(); - let outcome_spend_infos: Vec = (0..n_outcomes) - .map(|outcome_index| { - let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?; + let outcome_spend_infos: BTreeMap = params + .outcome_payouts + .iter() + .map(|(&outcome, payout_map)| { let winners = payout_map.keys().copied(); - - OutcomeSpendInfo::new( + let spend_info = OutcomeSpendInfo::new( winners, ¶ms.market_maker, outcome_value, params.relative_locktime_block_delta, - ) + )?; + Ok((outcome, spend_info)) }) .collect::>()?; - let outcome_txs: Vec = outcome_spend_infos + let outcome_txs: BTreeMap = outcome_spend_infos .iter() - .map(|outcome_spend_info| { + .map(|(&outcome, outcome_spend_info)| { let outcome_output = TxOut { value: outcome_value, script_pubkey: outcome_spend_info.script_pubkey(), }; - Transaction { + + let lock_time = match outcome { + Outcome::Expiry => LockTime::from_consensus(params.event.expiry), + Outcome::Attestation(_) => LockTime::ZERO, // Normal outcome transaction + }; + + let outcome_tx = Transaction { version: bitcoin::transaction::Version::TWO, - lock_time: LockTime::ZERO, + lock_time, input: vec![funding_input.clone()], output: vec![outcome_output], - } + }; + + (outcome, outcome_tx) }) .collect(); @@ -103,7 +115,7 @@ pub(crate) fn partial_sign_outcome_txs<'a>( seckey: Scalar, secnonces: impl IntoIterator, aggnonces: impl IntoIterator, -) -> Result, Error> { +) -> Result, Error> { let outcome_txs = &outcome_build_out.outcome_txs; let funding_spend_info = &outcome_build_out.funding_spend_info; @@ -113,36 +125,47 @@ pub(crate) fn partial_sign_outcome_txs<'a>( .pubkey_index(seckey.base_point_mul()) .ok_or(Error)?; - let n_outcomes = params.event.outcome_messages.len(); - let mut outcome_partial_sigs = Vec::with_capacity(n_outcomes); + let mut outcome_partial_sigs = BTreeMap::::new(); let mut aggnonce_iter = aggnonces.into_iter(); let mut secnonce_iter = secnonces.into_iter(); - for (outcome_index, outcome_tx) in outcome_txs.into_iter().enumerate() { + for (&outcome, outcome_tx) in outcome_txs { let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces let secnonce = secnonce_iter.next().ok_or(Error)?; // must provide enough secnonces - // All outcome TX signatures should be locked by the oracle's outcome point. - let attestation_lock_point = params - .event - .attestation_lock_point(outcome_index) - .ok_or(Error)?; - // Hash the outcome TX. let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?; - // partially sign the sighash. - let partial_sig = musig2::adaptor::sign_partial( - funding_spend_info.key_agg_ctx(), - seckey, - secnonce, - aggnonce, - attestation_lock_point, - sighash, - )?; + let partial_sig = match outcome { + Outcome::Attestation(outcome_index) => { + // All outcome TX signatures should be locked by the oracle's outcome point. + let attestation_lock_point = params + .event + .attestation_lock_point(outcome_index) + .ok_or(Error)?; - outcome_partial_sigs.push(partial_sig); + // sign under an attestation lock point + musig2::adaptor::sign_partial( + funding_spend_info.key_agg_ctx(), + seckey, + secnonce, + aggnonce, + attestation_lock_point, + sighash, + )? + } + + Outcome::Expiry => musig2::sign_partial( + funding_spend_info.key_agg_ctx(), + seckey, + secnonce, + aggnonce, + sighash, + )?, + }; + + outcome_partial_sigs.insert(outcome, partial_sig); } Ok(outcome_partial_sigs) } @@ -166,34 +189,63 @@ pub(crate) fn verify_outcome_tx_partial_signatures<'p, 'a>( let mut pubnonce_iter = pubnonces.into_iter(); let mut partial_sig_iter = partial_signatures.into_iter(); - for (outcome_index, outcome_tx) in outcome_txs.into_iter().enumerate() { + 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 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 // Hash the outcome TX. let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?; - // All outcome TX signatures should be locked by the oracle's outcome point. - let attestation_lock_point = params - .event - .attestation_lock_point(outcome_index) - .ok_or(Error)?; + match outcome { + Outcome::Attestation(outcome_index) => { + // All outcome TX signatures should be locked by the oracle's outcome point. + let attestation_lock_point = params + .event + .attestation_lock_point(outcome_index) + .ok_or(Error)?; - musig2::adaptor::verify_partial( - funding_spend_info.key_agg_ctx(), - partial_sig, - aggnonce, - attestation_lock_point, - player.pubkey, - pubnonce, - sighash, - )?; + musig2::adaptor::verify_partial( + funding_spend_info.key_agg_ctx(), + partial_sig, + aggnonce, + attestation_lock_point, + player.pubkey, + pubnonce, + sighash, + )?; + } + + Outcome::Expiry => { + musig2::verify_partial( + funding_spend_info.key_agg_ctx(), + partial_sig, + aggnonce, + player.pubkey, + pubnonce, + sighash, + )?; + } + }; } Ok(()) } +/// The result of aggregating signatures from all signers on all outcome transactions, +/// optionally including an expiry transaction. +#[derive(Clone, Debug)] +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, + + /// The complete signature on the expiry transaction. This is `None` if the + /// [`ContractParameters::outcome_payouts`] field does not contain an + /// [`Outcome::Expiry`] key. + pub(crate) expiry_tx_signature: Option, +} + /// Aggregate groups of partial signatures for all outcome transactions. /// /// Before running this method, the partial signatures should all have been @@ -208,7 +260,7 @@ pub(crate) fn aggregate_outcome_tx_adaptor_signatures<'a, S>( outcome_build_out: &OutcomeTransactionBuildOutput, aggnonces: impl IntoIterator, partial_signature_groups: impl IntoIterator, -) -> Result, Error> +) -> Result where S: IntoIterator, { @@ -218,34 +270,52 @@ where let mut aggnonce_iter = aggnonces.into_iter(); let mut partial_sig_group_iter = partial_signature_groups.into_iter(); - outcome_txs - .into_iter() - .enumerate() - .map(|(outcome_index, outcome_tx)| { - // must provide a set of sigs for each TX - let partial_sigs = partial_sig_group_iter.next().ok_or(Error)?; + let mut signatures = OutcomeSignatures { + adaptor_signatures: Vec::with_capacity(params.event.outcome_messages.len()), + expiry_tx_signature: None, + }; - let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces + 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 attestation_lock_point = params - .event - .attestation_lock_point(outcome_index) - .ok_or(Error)?; + let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces - // Hash the outcome TX. - let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?; + // Hash the outcome TX. + let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?; - let adaptor_sig = musig2::adaptor::aggregate_partial_signatures( - funding_spend_info.key_agg_ctx(), - aggnonce, - attestation_lock_point, - partial_sigs, - sighash, - )?; + match outcome { + Outcome::Attestation(outcome_index) => { + let attestation_lock_point = params + .event + .attestation_lock_point(outcome_index) + .ok_or(Error)?; - Ok(adaptor_sig) - }) - .collect() + let adaptor_sig = musig2::adaptor::aggregate_partial_signatures( + funding_spend_info.key_agg_ctx(), + aggnonce, + attestation_lock_point, + partial_sigs, + sighash, + )?; + + signatures.adaptor_signatures.push(adaptor_sig); + } + + Outcome::Expiry => { + let signature: CompactSignature = musig2::aggregate_partial_signatures( + funding_spend_info.key_agg_ctx(), + aggnonce, + partial_sigs, + sighash, + )?; + + signatures.expiry_tx_signature = Some(signature); + } + }; + } + + Ok(signatures) } /// Construct an input to spend an outcome transaction for a specific outcome. @@ -253,13 +323,10 @@ where /// to construct a set of [`bitcoin::sighash::Prevouts`]. pub(crate) fn outcome_tx_prevout<'x>( outcome_build_out: &'x OutcomeTransactionBuildOutput, - outcome_index: usize, + outcome: &Outcome, block_delay: u16, ) -> Result<(TxIn, &'x TxOut), Error> { - let outcome_tx = outcome_build_out - .outcome_txs() - .get(outcome_index) - .ok_or(Error)?; + let outcome_tx = outcome_build_out.outcome_txs().get(outcome).ok_or(Error)?; let outcome_input = TxIn { previous_output: OutPoint { @@ -270,7 +337,7 @@ pub(crate) fn outcome_tx_prevout<'x>( ..TxIn::default() }; - let prevout = outcome_tx.output.get(outcome_index).ok_or(Error)?; + let prevout = outcome_tx.output.get(0).ok_or(Error)?; Ok((outcome_input, prevout)) } diff --git a/src/contract/split.rs b/src/contract/split.rs index 74f571e..a3cbe35 100644 --- a/src/contract/split.rs +++ b/src/contract/split.rs @@ -5,7 +5,7 @@ use secp::Scalar; use crate::{ consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE}, contract::{self, fees, outcome::OutcomeTransactionBuildOutput}, - contract::{ContractParameters, WinCondition}, + contract::{ContractParameters, Outcome, WinCondition}, errors::Error, parties::Player, spend_info::SplitSpendInfo, @@ -17,14 +17,14 @@ use std::{borrow::Borrow, collections::BTreeMap}; /// This contains cached data used for constructing further transactions, /// or signing the split transactions themselves. pub(crate) struct SplitTransactionBuildOutput { - split_txs: Vec, + split_txs: BTreeMap, split_spend_infos: BTreeMap, } impl SplitTransactionBuildOutput { /// Return the set of mutually exclusive split transactions. Each of these /// transactions spend from a corresponding previous outcome transaction. - pub(crate) fn split_txs(&self) -> &[Transaction] { + pub(crate) fn split_txs(&self) -> &BTreeMap { &self.split_txs } @@ -43,16 +43,13 @@ pub(crate) fn build_split_txs( params: &ContractParameters, outcome_build_output: &OutcomeTransactionBuildOutput, ) -> Result { - let n_outcomes = params.outcome_payouts.len(); let mut split_spend_infos = BTreeMap::::new(); - let mut split_txs = Vec::::with_capacity(n_outcomes); - - for outcome_index in 0..params.outcome_payouts.len() { - let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?; + let mut split_txs = BTreeMap::::new(); + for (&outcome, payout_map) in params.outcome_payouts.iter() { let outcome_spend_info = &outcome_build_output .outcome_spend_infos() - .get(outcome_index) + .get(&outcome) .ok_or(Error)?; // Fee estimation @@ -69,7 +66,7 @@ pub(crate) fn build_split_txs( let (outcome_input, _) = contract::outcome::outcome_tx_prevout( outcome_build_output, - outcome_index, + &outcome, params.relative_locktime_block_delta, // Split TXs have 1*delta block delay )?; @@ -90,17 +87,19 @@ pub(crate) fn build_split_txs( let win_cond = WinCondition { winner: player, - outcome_index, + outcome, }; split_spend_infos.insert(win_cond, split_spend_info); } - split_txs.push(Transaction { + let split_tx = Transaction { version: bitcoin::transaction::Version::TWO, lock_time: LockTime::ZERO, input: vec![outcome_input], output: split_tx_outputs, - }); + }; + + split_txs.insert(outcome, split_tx); } let output = SplitTransactionBuildOutput { @@ -138,23 +137,24 @@ pub(crate) fn partial_sign_split_txs<'a>( return Ok(partial_signatures); } - let split_txs = &split_build_out.split_txs; - let mut aggnonce_iter = aggnonces.into_iter(); let mut secnonce_iter = secnonces.into_iter(); for win_cond in win_conditions_to_sign { - let split_tx = split_txs.get(win_cond.outcome_index).ok_or(Error)?; + let split_tx = split_build_out + .split_txs() + .get(&win_cond.outcome) + .ok_or(Error)?; let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces let secnonce = secnonce_iter.next().ok_or(Error)?; // must provide enough secnonces - // Hash the split TX. let outcome_spend_info = outcome_build_out .outcome_spend_infos() - .get(win_cond.outcome_index) + .get(&win_cond.outcome) .ok_or(Error)?; + // Hash the split TX. let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; // Partially sign the sighash. @@ -193,10 +193,11 @@ pub(crate) fn verify_split_tx_partial_signatures( .win_conditions_controlled_by_pubkey(player.pubkey) .ok_or(Error)?; - let split_txs = &split_build_out.split_txs; - for win_cond in win_conditions_to_sign { - let split_tx = split_txs.get(win_cond.outcome_index).ok_or(Error)?; + let split_tx = split_build_out + .split_txs() + .get(&win_cond.outcome) + .ok_or(Error)?; let aggnonce = aggnonces.get(&win_cond).ok_or(Error)?; // must provide all aggnonces let pubnonce = pubnonces.get(&win_cond).ok_or(Error)?; // must provide all pubnonces @@ -204,7 +205,7 @@ pub(crate) fn verify_split_tx_partial_signatures( let outcome_spend_info = outcome_build_out .outcome_spend_infos() - .get(win_cond.outcome_index) + .get(&win_cond.outcome) .ok_or(Error)?; // Hash the split TX. @@ -240,13 +241,14 @@ where &'s S: IntoIterator, P: Borrow, { - let split_txs = &split_build_out.split_txs; - split_build_out .split_spend_infos .keys() .map(|&win_cond| { - let split_tx = split_txs.get(win_cond.outcome_index).ok_or(Error)?; + let split_tx = split_build_out + .split_txs() + .get(&win_cond.outcome) + .ok_or(Error)?; let relevant_partial_sigs = partial_signatures_by_win_cond .get(&win_cond) @@ -258,7 +260,7 @@ where let outcome_spend_info = outcome_build_out .outcome_spend_infos() - .get(win_cond.outcome_index) + .get(&win_cond.outcome) .ok_or(Error)?; // Hash the split TX. @@ -282,17 +284,19 @@ where pub(crate) fn split_tx_prevout<'x>( params: &ContractParameters, split_build_out: &'x SplitTransactionBuildOutput, - outcome_index: usize, - winner: &Player, + win_cond: &WinCondition, block_delay: u16, ) -> Result<(TxIn, &'x TxOut), Error> { let split_tx = split_build_out .split_txs() - .get(outcome_index) + .get(&win_cond.outcome) .ok_or(Error)?; - let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?; - let split_tx_output_index = payout_map.keys().position(|p| p == winner).ok_or(Error)?; + let payout_map = params.outcome_payouts.get(&win_cond.outcome).ok_or(Error)?; + let split_tx_output_index = payout_map + .keys() + .position(|p| p == &win_cond.winner) + .ok_or(Error)?; let input = TxIn { previous_output: OutPoint {