diff --git a/src/contract/fees.rs b/src/contract/fees.rs index 6e03fa8..3e787a3 100644 --- a/src/contract/fees.rs +++ b/src/contract/fees.rs @@ -1,5 +1,7 @@ use bitcoin::{transaction::InputWeightPrediction, Amount, FeeRate}; +use std::collections::BTreeMap; + use crate::errors::Error; /// Compute the fee for a transaction given a fixed [`FeeRate`], input weights, @@ -35,3 +37,38 @@ pub(crate) fn fee_subtract_safe( } Ok(after_fee) } + +/// Safely compute the output amounts for a set of outputs by computing +/// and distributing the fee equally among all output values. +/// +/// 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>( + available_coins: Amount, + fee_rate: FeeRate, + input_weights: I, + output_spk_lens: O, + dust_threshold: Amount, + payout_map: &'k BTreeMap, +) -> Result, Error> +where + I: IntoIterator, + O: IntoIterator, + T: Clone + Ord, +{ + let fee_total = fee_calc_safe(fee_rate, input_weights, output_spk_lens)?; + + // Mining fees are distributed equally among all winners, regardless of payout weight. + let fee_shared = fee_total / payout_map.len() as u64; + let total_weight: u64 = payout_map.values().copied().sum(); + + // Payout amounts are computed by using relative weights. + payout_map + .iter() + .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)) + }) + .collect() +} diff --git a/src/contract/mod.rs b/src/contract/mod.rs index ef52e34..ff9f610 100644 --- a/src/contract/mod.rs +++ b/src/contract/mod.rs @@ -2,7 +2,7 @@ pub(crate) mod fees; pub(crate) mod outcome; pub(crate) mod split; -use bitcoin::{Amount, FeeRate}; +use bitcoin::{transaction::InputWeightPrediction, Amount, FeeRate}; use secp::Point; use crate::{ @@ -22,6 +22,9 @@ use std::collections::{BTreeMap, BTreeSet}; /// ```not_rust /// total_payout = contract_value * weights[player] / sum(weights) /// ``` +/// +/// Players who should not receive a payout from an outcome MUST NOT be given an entry +/// in a `PayoutWeights` map. pub type PayoutWeights = BTreeMap; /// Represents the parameters which all players and the market maker must agree on @@ -80,7 +83,7 @@ pub struct WinCondition { impl ContractParameters { pub(crate) fn outcome_output_value(&self) -> Result { - let input_weights = [bitcoin::transaction::InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH]; + let input_weights = [InputWeightPrediction::P2TR_KEY_DEFAULT_SIGHASH]; let fee = fees::fee_calc_safe(self.fee_rate, input_weights, [P2TR_SCRIPT_PUBKEY_SIZE])?; let outcome_value = fees::fee_subtract_safe(self.funding_value, fee, P2TR_DUST_VALUE)?; Ok(outcome_value) diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs index 1f0d6cc..84ee9ac 100644 --- a/src/contract/outcome.rs +++ b/src/contract/outcome.rs @@ -13,17 +13,29 @@ use crate::{ /// This contains cached data used for constructing further transactions, /// or signing the outcome transactions themselves. pub(crate) struct OutcomeTransactionBuildOutput { - pub(crate) outcome_txs: Vec, - pub(crate) outcome_spend_infos: Vec, + outcome_txs: Vec, + outcome_spend_infos: Vec, 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 fn outcome_txs(&self) -> &[Transaction] { + pub(crate) fn outcome_txs(&self) -> &[Transaction] { &self.outcome_txs } + + /// Return the set of mutually exclusive outcome spend info objects. + pub(crate) fn outcome_spend_infos(&self) -> &[OutcomeSpendInfo] { + &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`]. + pub(crate) fn signatures_required(&self) -> usize { + self.outcome_txs.len() + } } /// Construct a set of unsigned outcome transactions which spend from the funding TX. @@ -84,7 +96,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::outcome_txs`]. +/// checking the length of [`OutcomeTransactionBuildOutput::signatures_required`]. pub(crate) fn partial_sign_outcome_txs<'a>( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, @@ -139,7 +151,7 @@ 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::outcome_txs`]. +/// checking the length of [`OutcomeTransactionBuildOutput::signatures_required`]. pub(crate) fn verify_outcome_tx_partial_signatures<'p, 'a>( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, diff --git a/src/contract/split.rs b/src/contract/split.rs index 3358079..3bd9cb3 100644 --- a/src/contract/split.rs +++ b/src/contract/split.rs @@ -1,4 +1,4 @@ -use bitcoin::{absolute::LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut}; +use bitcoin::{absolute::LockTime, Amount, OutPoint, Sequence, Transaction, TxIn, TxOut}; use musig2::{AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; use secp::Scalar; @@ -24,9 +24,17 @@ pub(crate) struct SplitTransactionBuildOutput { impl SplitTransactionBuildOutput { /// Return the set of mutually exclusive split transactions. Each of these /// transactions spend from a corresponding previous outcome transaction. - pub fn split_txs(&self) -> &[Transaction] { + pub(crate) fn split_txs(&self) -> &[Transaction] { &self.split_txs } + + /// Returns the number of [`musig2`] partial signatures required by each player + /// in the DLC (and the market maker). This is the sum of all possible win conditions + /// across every split transaction (i.e. counting the number of winners in every + /// possible outcome). + pub(crate) fn signatures_required(&self) -> usize { + self.split_spend_infos.len() + } } /// Build the set of split transactions which splits an outcome TX into per-player @@ -43,18 +51,21 @@ pub(crate) fn build_split_txs( let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?; let outcome_spend_info = &outcome_build_output - .outcome_spend_infos + .outcome_spend_infos() .get(outcome_index) .ok_or(Error)?; // 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 fee_total = fees::fee_calc_safe(params.fee_rate, [input_weight], spk_lengths)?; - - // Mining fees are distributed equally among all winners, regardless of payout weight. - let fee_shared = fee_total / payout_map.len() as u64; - let total_payout_weight: u64 = payout_map.values().copied().sum(); + let payout_values: BTreeMap<&Player, Amount> = fees::fee_calc_shared( + outcome_spend_info.outcome_value(), + params.fee_rate, + [input_weight], + spk_lengths, + P2TR_DUST_VALUE, + &payout_map, + )?; let (outcome_input, _) = contract::outcome::outcome_tx_prevout( outcome_build_output, @@ -62,13 +73,9 @@ pub(crate) fn build_split_txs( params.relative_locktime_block_delta, // Split TXs have 1*delta block delay )?; - // payout_map is a btree, so outputs are automatically sorted by player. + // 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_weight) in payout_map.iter() { - // Payout amounts are computed by using relative weights. - let payout = outcome_spend_info.outcome_value() * payout_weight / total_payout_weight; - let payout_value = fees::fee_subtract_safe(payout, fee_shared, P2TR_DUST_VALUE)?; - + for (&&player, &payout_value) in payout_values.iter() { let split_spend_info = SplitSpendInfo::new( player, ¶ms.market_maker, @@ -145,7 +152,7 @@ pub(crate) fn partial_sign_split_txs<'a>( // Hash the split TX. let outcome_spend_info = outcome_build_out - .outcome_spend_infos + .outcome_spend_infos() .get(win_cond.outcome_index) .ok_or(Error)?; @@ -197,7 +204,7 @@ pub(crate) fn verify_split_tx_partial_signatures( let partial_sig = partial_signatures.get(&win_cond).copied().ok_or(Error)?; // must provide all sigs let outcome_spend_info = outcome_build_out - .outcome_spend_infos + .outcome_spend_infos() .get(win_cond.outcome_index) .ok_or(Error)?; @@ -251,7 +258,7 @@ where let aggnonce = aggnonces.get(&win_cond).ok_or(Error)?; let outcome_spend_info = outcome_build_out - .outcome_spend_infos + .outcome_spend_infos() .get(win_cond.outcome_index) .ok_or(Error)?;