reusable shared fee computation

This commit is contained in:
conduition
2024-02-18 21:18:07 +00:00
parent e3180febc6
commit 741a64085d
4 changed files with 83 additions and 24 deletions

View File

@@ -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<T, u64>,
) -> Result<BTreeMap<&'k T, Amount>, Error>
where
I: IntoIterator<Item = InputWeightPrediction>,
O: IntoIterator<Item = usize>,
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()
}

View File

@@ -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<Player, u64>;
/// 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<Amount, Error> {
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)

View File

@@ -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<Transaction>,
pub(crate) outcome_spend_infos: Vec<OutcomeSpendInfo>,
outcome_txs: Vec<Transaction>,
outcome_spend_infos: Vec<OutcomeSpendInfo>,
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,

View File

@@ -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,
&params.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)?;