Files
dlctix/src/contract/split.rs
conduition 3eb37c4d51 add serde serialization trait implementations
This allows players and the market maker to transact network sockets,
or other transport channels, by passing nonces, sigmaps, etc, back
and forth to each other. It also allows parties to store their signed
contract data locally, and enforce the contract even after a restart.
2024-03-16 20:27:09 +00:00

361 lines
13 KiB
Rust

use bitcoin::{absolute::LockTime, Amount, OutPoint, Sequence, Transaction, TxIn, TxOut};
use musig2::{
AggNonce, BatchVerificationRow, CompactSignature, PartialSignature, PubNonce, SecNonce,
};
use secp::{Point, Scalar};
use crate::{
consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE},
contract::{self, fees, outcome::OutcomeTransactionBuildOutput, PlayerIndex},
contract::{ContractParameters, Outcome, WinCondition},
errors::Error,
spend_info::SplitSpendInfo,
};
use std::collections::{BTreeMap, BTreeSet};
/// Represents the output of building the set of split transactions.
/// This contains cached data used for constructing further transactions,
/// or signing the split transactions themselves.
#[derive(Clone, Eq, PartialEq)]
pub(crate) struct SplitTransactionBuildOutput {
split_txs: BTreeMap<Outcome, Transaction>,
split_spend_infos: BTreeMap<WinCondition, SplitSpendInfo>,
}
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) -> &BTreeMap<Outcome, Transaction> {
&self.split_txs
}
/// Return the set of split spend info objects mapped by win condition.
pub(crate) fn split_spend_infos(&self) -> &BTreeMap<WinCondition, SplitSpendInfo> {
&self.split_spend_infos
}
}
/// Build the set of split transactions which splits an outcome TX into per-player
/// payout contracts between the player and the market maker.
pub(crate) fn build_split_txs(
params: &ContractParameters,
outcome_build_output: &OutcomeTransactionBuildOutput,
) -> Result<SplitTransactionBuildOutput, Error> {
let players = params.sorted_players();
let mut split_spend_infos = BTreeMap::<WinCondition, SplitSpendInfo>::new();
let mut split_txs = BTreeMap::<Outcome, Transaction>::new();
for (&outcome, payout_map) in params.outcome_payouts.iter() {
let outcome_spend_info = &outcome_build_output
.outcome_spend_infos()
.get(&outcome)
.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 payout_values: BTreeMap<PlayerIndex, 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,
&outcome,
params.relative_locktime_block_delta, // Split TXs have 1*delta block delay
)?;
// 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_index, payout_value) in payout_values {
let &player = players.get(player_index).ok_or(Error)?;
let split_spend_info = SplitSpendInfo::new(
player,
&params.market_maker,
params.relative_locktime_block_delta,
)?;
split_tx_outputs.push(TxOut {
value: payout_value,
script_pubkey: split_spend_info.script_pubkey(),
});
let win_cond = WinCondition {
player_index,
outcome,
};
split_spend_infos.insert(win_cond, split_spend_info);
}
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 {
split_txs,
split_spend_infos,
};
Ok(output)
}
/// Sign all split script spend paths for every split transaction needed.
///
/// Players only need to sign split transactions for outcomes in which
/// they are paid out by the DLC. Outcomes in which a player knows they
/// will not win any money are irrelevant to that player.
///
/// The market maker must sign every split script spending path of every
/// split transaction.
pub(crate) fn partial_sign_split_txs(
params: &ContractParameters,
outcome_build_out: &OutcomeTransactionBuildOutput,
split_build_out: &SplitTransactionBuildOutput,
seckey: Scalar,
mut secnonces: BTreeMap<WinCondition, SecNonce>,
aggnonces: &BTreeMap<WinCondition, AggNonce>,
) -> Result<BTreeMap<WinCondition, PartialSignature>, Error> {
let pubkey = seckey.base_point_mul();
let mut partial_signatures = BTreeMap::<WinCondition, PartialSignature>::new();
let win_conditions_to_sign = params
.win_conditions_controlled_by_pubkey(pubkey)
.ok_or(Error)?;
if win_conditions_to_sign.is_empty() {
return Ok(partial_signatures);
}
for win_cond in win_conditions_to_sign {
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 secnonce = secnonces.remove(&win_cond).ok_or(Error)?; // must provide all secnonces
let outcome_spend_info = outcome_build_out
.outcome_spend_infos()
.get(&win_cond.outcome)
.ok_or(Error)?;
// Hash the split TX.
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,
// because that's the key we pushed to the script.
let partial_sig = musig2::sign_partial(
outcome_spend_info.key_agg_ctx_untweaked(),
seckey,
secnonce,
aggnonce,
sighash,
)?;
partial_signatures.insert(win_cond, partial_sig);
}
Ok(partial_signatures)
}
/// Verify the partial signatures provided by a signer across every relevant split transaction.
///
/// Since each split transaction may require multiple signatures (one for each [`WinCondition`]),
/// the nonces and signatures must be provided in the form of a [`BTreeMap`], which maps
/// a [`WinCondition`] to the appropriate signature for that script spending path.
/// If any signatures or nonces are missing, this method returns an error.
pub(crate) fn verify_split_tx_partial_signatures(
params: &ContractParameters,
outcome_build_out: &OutcomeTransactionBuildOutput,
split_build_out: &SplitTransactionBuildOutput,
signer_pubkey: Point,
pubnonces: &BTreeMap<WinCondition, PubNonce>,
aggnonces: &BTreeMap<WinCondition, AggNonce>,
partial_signatures: &BTreeMap<WinCondition, PartialSignature>,
) -> Result<(), Error> {
let win_conditions_to_sign = params
.win_conditions_controlled_by_pubkey(signer_pubkey)
.ok_or(Error)?;
for win_cond in win_conditions_to_sign {
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
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()
.get(&win_cond.outcome)
.ok_or(Error)?;
// Hash the split TX.
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(
outcome_spend_info.key_agg_ctx_untweaked(),
partial_sig,
aggnonce,
signer_pubkey,
pubnonce,
sighash,
)?;
}
Ok(())
}
/// Aggregate all partial signatures on every spending path of all split transactions.
///
/// Since each split transaction may require multiple signatures (one for each [`WinCondition`]),
/// the nonces and signatures must be provided in the form of a [`BTreeMap`], which maps
/// a [`WinCondition`] to the appropriate signature for that script spending path.
/// If any signatures or nonces are missing, this method returns an error.
pub(crate) fn aggregate_split_tx_signatures<S>(
outcome_build_out: &OutcomeTransactionBuildOutput,
split_build_out: &SplitTransactionBuildOutput,
aggnonces: &BTreeMap<WinCondition, AggNonce>,
mut partial_signatures_by_win_cond: BTreeMap<WinCondition, S>,
) -> Result<BTreeMap<WinCondition, CompactSignature>, Error>
where
S: IntoIterator<Item = PartialSignature>,
{
split_build_out
.split_spend_infos
.keys()
.map(|&win_cond| {
let split_tx = split_build_out
.split_txs()
.get(&win_cond.outcome)
.ok_or(Error)?;
let relevant_partial_sigs = partial_signatures_by_win_cond
.remove(&win_cond)
.ok_or(Error)?;
let aggnonce = aggnonces.get(&win_cond).ok_or(Error)?;
let outcome_spend_info = outcome_build_out
.outcome_spend_infos()
.get(&win_cond.outcome)
.ok_or(Error)?;
// Hash the split TX.
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(),
aggnonce,
relevant_partial_sigs,
sighash,
)?;
Ok((win_cond, compact_sig))
})
.collect()
}
/// 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<WinCondition, CompactSignature>,
) -> Result<(), Error> {
// We only need to verify signatures on win conditions where our pubkey might
// win something.
let relevant_win_conditions: BTreeSet<WinCondition> = params
.win_conditions_claimable_by_pubkey(our_pubkey)
.ok_or(Error)?;
let batch: Vec<BatchVerificationRow> = relevant_win_conditions
.into_iter()
.map(|win_cond| {
let split_tx = split_build_out
.split_txs()
.get(&win_cond.outcome)
.ok_or(Error)?;
let signature = split_tx_signatures.get(&win_cond).ok_or(Error)?;
let outcome_spend_info = outcome_build_out
.outcome_spend_infos()
.get(&win_cond.outcome)
.ok_or(Error)?;
// Expect an untweaked signature by the group, so that the signature
// can be used to trigger one of the players' split tapscripts.
let winners_joint_pubkey: Point = outcome_spend_info
.key_agg_ctx_untweaked()
.aggregated_pubkey();
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?;
let batch_row = BatchVerificationRow::from_signature(
winners_joint_pubkey,
sighash,
signature.lift_nonce()?,
);
Ok(batch_row)
})
.collect::<Result<_, Error>>()?;
musig2::verify_batch(&batch)?;
Ok(())
}
/// Construct an input to spend a given player's output of the split transaction
/// for a specific outcome. Also returns a reference to the split TX's output so
/// it can be used to construct a set of [`bitcoin::sighash::Prevouts`].
pub(crate) fn split_tx_prevout<'x>(
params: &ContractParameters,
split_build_out: &'x SplitTransactionBuildOutput,
win_cond: &WinCondition,
block_delay: u16,
) -> Result<(TxIn, &'x TxOut), Error> {
let split_tx = split_build_out
.split_txs()
.get(&win_cond.outcome)
.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(|&player_index| player_index == win_cond.player_index)
.ok_or(Error)?;
let input = TxIn {
previous_output: OutPoint {
txid: split_tx.txid(),
vout: split_tx_output_index as u32,
},
sequence: Sequence::from_height(block_delay),
..TxIn::default()
};
let prevout = split_tx.output.get(split_tx_output_index).ok_or(Error)?;
Ok((input, prevout))
}