diff --git a/src/contract/fees.rs b/src/contract/fees.rs index 3e787a3..d227f29 100644 --- a/src/contract/fees.rs +++ b/src/contract/fees.rs @@ -43,18 +43,18 @@ pub(crate) fn fee_subtract_safe( /// /// Returns an error if any output value is negative, or is less than the /// given dust threshold. -pub(crate) fn fee_calc_shared<'k, I, O, T>( +pub(crate) fn fee_calc_shared( available_coins: Amount, fee_rate: FeeRate, input_weights: I, output_spk_lens: O, dust_threshold: Amount, - payout_map: &'k BTreeMap, -) -> Result, Error> + payout_map: &BTreeMap, +) -> Result, Error> where I: IntoIterator, O: IntoIterator, - T: Clone + Ord, + T: Copy + Ord, { let fee_total = fee_calc_safe(fee_rate, input_weights, output_spk_lens)?; @@ -65,7 +65,7 @@ where // Payout amounts are computed by using relative weights. payout_map .iter() - .map(|(key, &weight)| { + .map(|(&key, &weight)| { let payout = available_coins * weight / total_weight; let payout_value = fee_subtract_safe(payout, fee_shared, dust_threshold)?; Ok((key, payout_value)) diff --git a/src/contract/mod.rs b/src/contract/mod.rs index e0a84a1..79b8be6 100644 --- a/src/contract/mod.rs +++ b/src/contract/mod.rs @@ -15,6 +15,10 @@ use crate::{ use std::collections::{BTreeMap, BTreeSet}; +/// A type alias for clarity. Players in the DLC are often referred to by their +/// index in the sorted set of players. +pub type PlayerIndex = usize; + /// Represents a mapping of player to payout weight for a given outcome. /// /// A player's payout under an outcome is proportional to the size of their payout weight @@ -26,7 +30,7 @@ use std::collections::{BTreeMap, BTreeSet}; /// /// Players who should not receive a payout from an outcome MUST NOT be given an entry /// in a `PayoutWeights` map. -pub type PayoutWeights = BTreeMap; +pub type PayoutWeights = BTreeMap; /// Represents the parameters which all players and the market maker must agree on /// to construct a ticketed DLC. @@ -89,7 +93,7 @@ pub enum Outcome { #[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] pub struct WinCondition { pub outcome: Outcome, - pub winner: Player, + pub player_index: PlayerIndex, } impl ContractParameters { @@ -120,14 +124,14 @@ impl ContractParameters { return Err(Error); } - for (player, &weight) in payout_map.iter() { + for (&player_index, &weight) in payout_map.iter() { // Check for zero payout weights. if weight == 0 { return Err(Error); } - // Check for unregistered players. - if !self.players.contains(player) { + // Check for out-of-bounds player indexes. + if player_index >= self.players.len() { return Err(Error); } } @@ -151,6 +155,12 @@ impl ContractParameters { Ok(()) } + /// Return a sorted vector of players. Each player's index in this vector + /// should be used as an identifier for the DLC. + pub fn sorted_players<'a>(&'a self) -> Vec<&'a Player> { + self.players.iter().collect() + } + /// Returns the transaction output which the funding transaction should pay to. /// /// Avoid overusing this method, as it recomputes the aggregated key every time @@ -169,14 +179,21 @@ impl ContractParameters { Ok(outcome_value) } - /// Returns the set of players which this pubkey can sign for. + /// Returns the set of player indexes 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> { + pub fn players_controlled_by_pubkey(&self, pubkey: Point) -> BTreeSet { self.players .iter() - .filter(|player| player.pubkey == pubkey) + .enumerate() + .filter_map(|(i, player)| { + if player.pubkey == pubkey { + Some(i) + } else { + None + } + }) .collect() } @@ -211,12 +228,16 @@ impl ContractParameters { 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 }), - ); + win_conditions_to_sign.extend(payout_map.keys().filter_map(|player_index| { + if is_market_maker || controlling_players.contains(player_index) { + Some(WinCondition { + player_index: *player_index, + outcome, + }) + } else { + None + } + })); } Some(win_conditions_to_sign) @@ -239,11 +260,10 @@ impl ContractParameters { 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 }), - ); + all_win_conditions.extend(payout_map.keys().map(|&player_index| WinCondition { + player_index, + outcome, + })); } all_win_conditions } diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs index d7270c3..6d009a2 100644 --- a/src/contract/outcome.rs +++ b/src/contract/outcome.rs @@ -45,6 +45,8 @@ pub(crate) fn build_outcome_txs( params: &ContractParameters, funding_outpoint: OutPoint, ) -> Result { + let all_players = params.sorted_players(); + let funding_input = TxIn { previous_output: funding_outpoint, sequence: Sequence::MAX, @@ -58,6 +60,7 @@ pub(crate) fn build_outcome_txs( .map(|(&outcome, payout_map)| { let winners = payout_map.keys().copied(); let spend_info = OutcomeSpendInfo::new( + &all_players, winners, ¶ms.market_maker, outcome_value, diff --git a/src/contract/split.rs b/src/contract/split.rs index 0ff72ab..c5af0e8 100644 --- a/src/contract/split.rs +++ b/src/contract/split.rs @@ -6,10 +6,9 @@ use secp::{Point, Scalar}; use crate::{ consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE}, - contract::{self, fees, outcome::OutcomeTransactionBuildOutput}, + contract::{self, fees, outcome::OutcomeTransactionBuildOutput, PlayerIndex}, contract::{ContractParameters, Outcome, WinCondition}, errors::Error, - parties::Player, spend_info::SplitSpendInfo, }; @@ -42,6 +41,8 @@ pub(crate) fn build_split_txs( params: &ContractParameters, outcome_build_output: &OutcomeTransactionBuildOutput, ) -> Result { + let players = params.sorted_players(); + let mut split_spend_infos = BTreeMap::::new(); let mut split_txs = BTreeMap::::new(); @@ -54,7 +55,7 @@ pub(crate) fn build_split_txs( // Fee estimation let input_weight = outcome_spend_info.input_weight_for_split_tx(); let spk_lengths = std::iter::repeat(P2TR_SCRIPT_PUBKEY_SIZE).take(payout_map.len()); - let payout_values: BTreeMap<&Player, Amount> = fees::fee_calc_shared( + let payout_values: BTreeMap = fees::fee_calc_shared( outcome_spend_info.outcome_value(), params.fee_rate, [input_weight], @@ -71,7 +72,8 @@ pub(crate) fn build_split_txs( // payout_values is a btree, so outputs are automatically sorted by player. let mut split_tx_outputs = Vec::with_capacity(payout_map.len()); - for (&&player, &payout_value) in payout_values.iter() { + for (player_index, payout_value) in payout_values { + let &player = players.get(player_index).ok_or(Error)?; let split_spend_info = SplitSpendInfo::new( player, ¶ms.market_maker, @@ -85,7 +87,7 @@ pub(crate) fn build_split_txs( }); let win_cond = WinCondition { - winner: player, + player_index, outcome, }; split_spend_infos.insert(win_cond, split_spend_info); @@ -151,7 +153,7 @@ pub(crate) fn partial_sign_split_txs( .ok_or(Error)?; // Hash the split TX. - let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?; // Partially sign the sighash. // We must use the untweaked musig key to sign the split script spend, @@ -205,7 +207,7 @@ pub(crate) fn verify_split_tx_partial_signatures( .ok_or(Error)?; // Hash the split TX. - let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?; // Verifies the player's partial signature on the split TX for one specific script path spend. musig2::verify_partial( @@ -257,7 +259,7 @@ where .ok_or(Error)?; // Hash the split TX. - let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?; let compact_sig = musig2::aggregate_partial_signatures( outcome_spend_info.key_agg_ctx_untweaked(), @@ -306,7 +308,7 @@ pub(crate) fn verify_split_tx_aggregated_signatures( .key_agg_ctx_untweaked() .aggregated_pubkey(); - let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?; let batch_row = BatchVerificationRow::from_signature( winners_joint_pubkey, @@ -340,7 +342,7 @@ pub(crate) fn split_tx_prevout<'x>( 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) + .position(|&player_index| player_index == win_cond.player_index) .ok_or(Error)?; let input = TxIn { diff --git a/src/lib.rs b/src/lib.rs index 1e0e32d..7118690 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -27,7 +27,7 @@ use std::{ collections::{BTreeMap, BTreeSet}, }; -pub use contract::{ContractParameters, Outcome, SigMap, WinCondition}; +pub use contract::{ContractParameters, Outcome, PlayerIndex, SigMap, WinCondition}; pub use oracles::EventAnnouncement; pub use parties::{MarketMaker, Player}; @@ -561,9 +561,17 @@ impl SignedContract { win_cond: &WinCondition, ticket_preimage: Preimage, ) -> Result { + let winner = self + .dlc + .params + .sorted_players() + .get(win_cond.player_index) + .cloned() + .ok_or(Error)?; + // Verify the preimage will unlock this specific player's split TX // condition. - if sha256(&ticket_preimage) != win_cond.winner.ticket_hash { + if sha256(&ticket_preimage) != winner.ticket_hash { return Err(Error); } @@ -580,8 +588,11 @@ impl SignedContract { .get(&win_cond.outcome) .ok_or(Error)?; - let witness = - outcome_spend_info.witness_tx_split(signature, ticket_preimage, &win_cond.winner)?; + let witness = outcome_spend_info.witness_tx_split( + signature, + ticket_preimage, + &win_cond.player_index, + )?; let mut split_tx = self .unsigned_split_tx(&win_cond.outcome) @@ -693,10 +704,18 @@ impl SignedContract { ticket_preimage: Preimage, player_secret_key: impl Into, ) -> Result<(), Error> { + let winner = self + .dlc + .params + .sorted_players() + .get(win_cond.player_index) + .cloned() + .ok_or(Error)?; + let player_secret_key = player_secret_key.into(); - if player_secret_key.base_point_mul() != win_cond.winner.pubkey { + if player_secret_key.base_point_mul() != winner.pubkey { return Err(Error); - } else if sha256(&ticket_preimage) != win_cond.winner.ticket_hash { + } else if sha256(&ticket_preimage) != winner.ticket_hash { return Err(Error); } diff --git a/src/parties.rs b/src/parties.rs index 7187a14..d0ae3a1 100644 --- a/src/parties.rs +++ b/src/parties.rs @@ -17,7 +17,7 @@ pub struct MarketMaker { /// ticket hashes, so players might share common pubkeys. However, for the /// economics of the contract to work, every player should be allocated /// their own completely unique ticket hash. -#[derive(Debug, Clone, Copy, Ord, PartialOrd, Hash, Eq, PartialEq)] +#[derive(Debug, Clone, Ord, PartialOrd, Hash, Eq, PartialEq)] pub struct Player { /// An ephemeral public key controlled by the player. /// diff --git a/src/spend_info/outcome.rs b/src/spend_info/outcome.rs index 3c44fff..ebb1e75 100644 --- a/src/spend_info/outcome.rs +++ b/src/spend_info/outcome.rs @@ -12,6 +12,7 @@ use musig2::{CompactSignature, KeyAggContext}; use secp::{Point, Scalar}; use crate::{ + contract::PlayerIndex, errors::Error, hashlock::{Preimage, PREIMAGE_SIZE}, parties::{MarketMaker, Player}, @@ -40,29 +41,37 @@ pub(crate) struct OutcomeSpendInfo { tweaked_ctx: KeyAggContext, outcome_value: Amount, spend_info: TaprootSpendInfo, - winner_split_scripts: BTreeMap, + winner_split_scripts: BTreeMap, reclaim_script: ScriptBuf, } impl OutcomeSpendInfo { - pub(crate) fn new>( - winners: W, + pub(crate) fn new( + all_players: &[impl Borrow], + winner_indexes: impl IntoIterator, market_maker: &MarketMaker, outcome_value: Amount, block_delta: u16, ) -> Result { - let winners: Vec = winners.into_iter().collect(); + let winners: BTreeMap = winner_indexes + .into_iter() + .map(|i| { + let player = all_players.get(i).ok_or(Error)?; + Ok((i, player.borrow())) + }) + .collect::>()?; + let mut pubkeys: Vec = [market_maker.pubkey] .into_iter() - .chain(winners.iter().map(|w| w.pubkey)) + .chain(winners.values().map(|w| w.pubkey)) .collect(); pubkeys.sort(); let untweaked_ctx = KeyAggContext::new(pubkeys)?; let joint_outcome_pubkey: Point = untweaked_ctx.aggregated_pubkey(); - let winner_split_scripts: BTreeMap = winners - .iter() - .map(|&winner| { + let winner_split_scripts: BTreeMap = winners + .into_iter() + .map(|(player_index, winner)| { // The winner split script, used by winning players to spend // the outcome transaction using the split transaction. // @@ -79,7 +88,7 @@ impl OutcomeSpendInfo { // Sequence number is enforced by multisig key: split TX is pre-signed. .into_script(); - (winner, script) + (player_index, script) }) .collect(); @@ -202,13 +211,13 @@ impl OutcomeSpendInfo { pub(crate) fn sighash_tx_split( &self, split_tx: &Transaction, - winner: &Player, + player_index: &PlayerIndex, ) -> Result { let outcome_prevouts = [TxOut { script_pubkey: self.script_pubkey(), value: self.outcome_value, }]; - let split_script = self.winner_split_scripts.get(winner).ok_or(Error)?; + let split_script = self.winner_split_scripts.get(player_index).ok_or(Error)?; let leaf_hash = TapLeafHash::from_script(split_script, LeafVersion::TapScript); let sighash = SighashCache::new(split_tx).taproot_script_spend_signature_hash( @@ -225,9 +234,13 @@ impl OutcomeSpendInfo { &self, signature: &CompactSignature, ticket_preimage: Preimage, - winner: &Player, + player_index: &PlayerIndex, ) -> Result { - let split_script = self.winner_split_scripts.get(winner).ok_or(Error)?.clone(); + let split_script = self + .winner_split_scripts + .get(player_index) + .ok_or(Error)? + .clone(); let control_block = self .spend_info .control_block(&(split_script.clone(), LeafVersion::TapScript)) diff --git a/src/spend_info/split.rs b/src/spend_info/split.rs index 44a73af..f51942c 100644 --- a/src/spend_info/split.rs +++ b/src/spend_info/split.rs @@ -32,7 +32,6 @@ pub(crate) struct SplitSpendInfo { tweaked_ctx: KeyAggContext, payout_value: Amount, spend_info: TaprootSpendInfo, - winner: Player, win_script: ScriptBuf, reclaim_script: ScriptBuf, sellback_script: ScriptBuf, @@ -40,7 +39,7 @@ pub(crate) struct SplitSpendInfo { impl SplitSpendInfo { pub(crate) fn new( - winner: Player, + winner: &Player, market_maker: &MarketMaker, payout_value: Amount, block_delta: u16, @@ -120,7 +119,6 @@ impl SplitSpendInfo { tweaked_ctx, payout_value, spend_info: tr_spend_info, - winner, win_script, reclaim_script, sellback_script,