mirror of
https://github.com/conduition/dlctix.git
synced 2025-12-17 00:24:20 +01:00
401 lines
14 KiB
Rust
401 lines
14 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 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::UnknownOutcome)?;
|
|
|
|
// 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 = params
|
|
.players
|
|
.get(player_index)
|
|
.ok_or(Error::OutOfBoundsPlayerIndex)?;
|
|
let split_spend_info = SplitSpendInfo::new(
|
|
player,
|
|
¶ms.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::InvalidKey)?;
|
|
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::UnknownOutcome)?;
|
|
|
|
let aggnonce = aggnonces
|
|
.get(&win_cond)
|
|
.ok_or(Error::MissingNonce(String::from(
|
|
"aggnonce for win condition",
|
|
)))?; // must provide all aggnonces
|
|
let secnonce = secnonces
|
|
.remove(&win_cond)
|
|
.ok_or(Error::MissingNonce(String::from(
|
|
"secnonces for win condition",
|
|
)))?; // must provide all secnonces
|
|
|
|
let outcome_spend_info = outcome_build_out
|
|
.outcome_spend_infos()
|
|
.get(&win_cond.outcome)
|
|
.ok_or(Error::UnknownOutcome)?;
|
|
|
|
// 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::InvalidKey)?;
|
|
|
|
for win_cond in win_conditions_to_sign {
|
|
let split_tx = split_build_out
|
|
.split_txs()
|
|
.get(&win_cond.outcome)
|
|
.ok_or(Error::UnknownOutcome)?;
|
|
|
|
let aggnonce = aggnonces
|
|
.get(&win_cond)
|
|
.ok_or(Error::MissingNonce(String::from(
|
|
"aggnonces for win condition",
|
|
)))?; // must provide all aggnonces
|
|
let pubnonce = pubnonces
|
|
.get(&win_cond)
|
|
.ok_or(Error::MissingNonce(String::from(
|
|
"pubnonce for win condition",
|
|
)))?; // must provide all pubnonces
|
|
let partial_sig =
|
|
partial_signatures
|
|
.get(&win_cond)
|
|
.copied()
|
|
.ok_or(Error::MissingSignature(String::from(
|
|
"partial signatures for win condition",
|
|
)))?;
|
|
|
|
let outcome_spend_info = outcome_build_out
|
|
.outcome_spend_infos()
|
|
.get(&win_cond.outcome)
|
|
.ok_or(Error::UnknownOutcome)?;
|
|
|
|
// 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::UnknownOutcome)?;
|
|
|
|
let relevant_partial_sigs =
|
|
partial_signatures_by_win_cond
|
|
.remove(&win_cond)
|
|
.ok_or(Error::MissingSignature(String::from(
|
|
"partial signatures by win cond for win condition",
|
|
)))?;
|
|
|
|
let aggnonce = aggnonces
|
|
.get(&win_cond)
|
|
.ok_or(Error::MissingNonce(String::from(
|
|
"aggnonces for win condition",
|
|
)))?;
|
|
|
|
let outcome_spend_info = outcome_build_out
|
|
.outcome_spend_infos()
|
|
.get(&win_cond.outcome)
|
|
.ok_or(Error::UnknownOutcome)?;
|
|
|
|
// 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::InvalidKey)?;
|
|
|
|
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::UnknownOutcome)?;
|
|
|
|
let signature = split_tx_signatures
|
|
.get(&win_cond)
|
|
.ok_or(Error::MissingSignature(String::from(
|
|
"split tx signature for win condition",
|
|
)))?;
|
|
|
|
let outcome_spend_info = outcome_build_out
|
|
.outcome_spend_infos()
|
|
.get(&win_cond.outcome)
|
|
.ok_or(Error::UnknownOutcome)?;
|
|
|
|
// 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::UnknownOutcome)?;
|
|
|
|
let payout_map = params
|
|
.outcome_payouts
|
|
.get(&win_cond.outcome)
|
|
.ok_or(Error::UnknownOutcome)?;
|
|
let split_tx_output_index = payout_map
|
|
.keys()
|
|
.position(|&player_index| player_index == win_cond.player_index)
|
|
.ok_or(Error::OutOfBoundsPlayerIndex)?;
|
|
|
|
let input = TxIn {
|
|
previous_output: OutPoint {
|
|
txid: split_tx.compute_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::InvalidInput("missing split tx output"))?;
|
|
|
|
Ok((input, prevout))
|
|
}
|