Files
dlctix/src/contract/outcome.rs
2025-07-21 15:24:21 -07:00

437 lines
15 KiB
Rust

use bitcoin::{absolute::LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut};
use musig2::{
AdaptorSignature, AggNonce, BatchVerificationRow, CompactSignature, PartialSignature, PubNonce,
SecNonce,
};
use secp::{Point, Scalar};
use std::collections::{BTreeMap, BTreeSet};
use crate::{
contract::{ContractParameters, Outcome, OutcomeIndex},
errors::Error,
spend_info::{FundingSpendInfo, OutcomeSpendInfo},
};
/// Represents the output of building the set of outcome transactions.
/// This contains cached data used for constructing further transactions,
/// or signing the outcome transactions themselves.
#[derive(Clone, Eq, PartialEq)]
pub(crate) struct OutcomeTransactionBuildOutput {
outcome_txs: BTreeMap<Outcome, Transaction>,
outcome_spend_infos: BTreeMap<Outcome, 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(crate) fn outcome_txs(&self) -> &BTreeMap<Outcome, Transaction> {
&self.outcome_txs
}
/// Return the set of mutually exclusive outcome spend info objects.
pub(crate) fn outcome_spend_infos(&self) -> &BTreeMap<Outcome, OutcomeSpendInfo> {
&self.outcome_spend_infos
}
/// Return the funding transaction's spending info object.
pub(crate) fn funding_spend_info(&self) -> &FundingSpendInfo {
&self.funding_spend_info
}
}
/// Construct a set of unsigned outcome transactions which spend from the funding TX.
pub(crate) fn build_outcome_txs(
params: &ContractParameters,
funding_outpoint: OutPoint,
) -> Result<OutcomeTransactionBuildOutput, Error> {
let funding_input = TxIn {
previous_output: funding_outpoint,
sequence: Sequence::ENABLE_LOCKTIME_NO_RBF,
..TxIn::default()
};
let outcome_value = params.outcome_output_value()?;
let outcome_spend_infos: BTreeMap<Outcome, OutcomeSpendInfo> = params
.outcome_payouts
.iter()
.map(|(&outcome, payout_map)| {
let winners = payout_map.keys().copied();
let spend_info = OutcomeSpendInfo::new(
&params.players,
winners,
&params.market_maker,
outcome_value,
params.relative_locktime_block_delta,
)?;
Ok((outcome, spend_info))
})
.collect::<Result<_, Error>>()?;
let outcome_txs: BTreeMap<Outcome, Transaction> = outcome_spend_infos
.iter()
.map(|(&outcome, outcome_spend_info)| {
let outcome_output = TxOut {
value: outcome_value,
script_pubkey: outcome_spend_info.script_pubkey(),
};
let lock_time = match outcome {
Outcome::Expiry => {
LockTime::from_consensus(params.event.expiry.ok_or(Error::InvalidLocktime)?)
}
Outcome::Attestation(_) => LockTime::ZERO, // Normal outcome transaction
};
let outcome_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time,
input: vec![funding_input.clone()],
output: vec![outcome_output],
};
Ok((outcome, outcome_tx))
})
.collect::<Result<_, Error>>()?;
let funding_spend_info =
FundingSpendInfo::new(&params.market_maker, &params.players, params.funding_value)?;
let output = OutcomeTransactionBuildOutput {
outcome_txs,
outcome_spend_infos,
funding_spend_info,
};
Ok(output)
}
/// Construct a set of partial signatures for the outcome transactions.
pub(crate) fn partial_sign_outcome_txs(
params: &ContractParameters,
outcome_build_out: &OutcomeTransactionBuildOutput,
seckey: Scalar,
mut secnonces: BTreeMap<Outcome, SecNonce>,
aggnonces: &BTreeMap<Outcome, AggNonce>,
) -> Result<BTreeMap<Outcome, PartialSignature>, Error> {
let outcome_txs = &outcome_build_out.outcome_txs;
let funding_spend_info = &outcome_build_out.funding_spend_info;
// Confirm the key is a part of the group.
funding_spend_info
.key_agg_ctx()
.pubkey_index(seckey.base_point_mul())
.ok_or(Error::InvalidKey)?;
let mut outcome_partial_sigs = BTreeMap::<Outcome, PartialSignature>::new();
for (&outcome, outcome_tx) in outcome_txs {
let aggnonce = aggnonces
.get(&outcome)
.ok_or(Error::MissingNonce(String::from("aggnonce for outcome")))?; // must provide all aggnonces
let secnonce = secnonces
.remove(&outcome)
.ok_or(Error::MissingNonce(String::from("secnonce for outcome")))?; // must provide all secnonces
// Hash the outcome TX.
let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?;
let partial_sig = match outcome {
Outcome::Attestation(outcome_index) => {
// All outcome TX signatures should be locked by the oracle's outcome point.
let attestation_lock_point = params
.event
.locking_points
.get(outcome_index)
.ok_or(Error::UnknownOutcome)?;
// sign under an attestation lock point
musig2::adaptor::sign_partial(
funding_spend_info.key_agg_ctx(),
seckey,
secnonce,
aggnonce,
*attestation_lock_point,
sighash,
)?
}
Outcome::Expiry => musig2::sign_partial(
funding_spend_info.key_agg_ctx(),
seckey,
secnonce,
aggnonce,
sighash,
)?,
};
outcome_partial_sigs.insert(outcome, partial_sig);
}
Ok(outcome_partial_sigs)
}
/// Verify a player's partial adaptor signatures on the outcome transactions.
pub(crate) fn verify_outcome_tx_partial_signatures(
params: &ContractParameters,
outcome_build_out: &OutcomeTransactionBuildOutput,
signer_pubkey: Point,
pubnonces: &BTreeMap<Outcome, PubNonce>,
aggnonces: &BTreeMap<Outcome, AggNonce>,
partial_signatures: &BTreeMap<Outcome, PartialSignature>,
) -> Result<(), Error> {
let outcome_txs = &outcome_build_out.outcome_txs;
let funding_spend_info = &outcome_build_out.funding_spend_info;
for (&outcome, outcome_tx) in outcome_txs {
let aggnonce = aggnonces
.get(&outcome)
.ok_or(Error::MissingNonce(String::from("aggnonce for outcome")))?; // must provide all aggnonces
let pubnonce = pubnonces
.get(&outcome)
.ok_or(Error::MissingNonce(String::from("pubnonce for outcome")))?; // must provide all pubnonces
let &partial_sig = partial_signatures
.get(&outcome)
.ok_or(Error::MissingSignature(String::from(
"partial_signatures for outcome",
)))?; // must provide all sigs
// Hash the outcome TX.
let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?;
match outcome {
Outcome::Attestation(outcome_index) => {
// All outcome TX signatures should be locked by the oracle's outcome point.
let attestation_lock_point = params
.event
.locking_points
.get(outcome_index)
.ok_or(Error::UnknownOutcome)?;
musig2::adaptor::verify_partial(
funding_spend_info.key_agg_ctx(),
partial_sig,
aggnonce,
*attestation_lock_point,
signer_pubkey,
pubnonce,
sighash,
)?;
}
Outcome::Expiry => {
musig2::verify_partial(
funding_spend_info.key_agg_ctx(),
partial_sig,
aggnonce,
signer_pubkey,
pubnonce,
sighash,
)?;
}
};
}
Ok(())
}
/// The result of aggregating signatures from all signers on all outcome transactions,
/// optionally including an expiry transaction.
#[derive(Clone, Debug)]
pub(crate) struct OutcomeSignatures {
/// A set of adaptor signatures which can be unlocked by the oracle's attestation
/// for each outcome.
pub(crate) outcome_tx_signatures: BTreeMap<OutcomeIndex, AdaptorSignature>,
/// The complete signature on the expiry transaction. This is `None` if the
/// [`ContractParameters::outcome_payouts`] field does not contain an
/// [`Outcome::Expiry`] key.
pub(crate) expiry_tx_signature: Option<CompactSignature>,
}
/// Aggregate groups of partial signatures for all outcome transactions.
///
/// Before running this method, the partial signatures should all have been
/// individually verified so that any blame can be assigned to signers
/// who submitted invalid signatures.
///
/// If all partial signatures are valid, then aggregation succeeds and this
/// function outputs a set of adaptor signatures which are valid once adapted
/// with the oracle's attestation.
pub(crate) fn aggregate_outcome_tx_adaptor_signatures<S>(
params: &ContractParameters,
outcome_build_out: &OutcomeTransactionBuildOutput,
aggnonces: &BTreeMap<Outcome, AggNonce>,
mut partial_signature_groups: BTreeMap<Outcome, S>,
) -> Result<OutcomeSignatures, Error>
where
S: IntoIterator<Item = PartialSignature>,
{
let outcome_txs = &outcome_build_out.outcome_txs;
let funding_spend_info = &outcome_build_out.funding_spend_info;
let mut signatures = OutcomeSignatures {
outcome_tx_signatures: BTreeMap::new(),
expiry_tx_signature: None,
};
for (&outcome, outcome_tx) in outcome_txs {
// must provide a set of sigs for each TX
let partial_sigs =
partial_signature_groups
.remove(&outcome)
.ok_or(Error::MissingSignature(String::from(
"outcome from partial signature groups",
)))?;
// must provide all aggnonces
let aggnonce = aggnonces
.get(&outcome)
.ok_or(Error::MissingNonce(String::from("aggnonces for outcome")))?;
// Hash the outcome TX.
let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?;
match outcome {
Outcome::Attestation(outcome_index) => {
let attestation_lock_point = params
.event
.locking_points
.get(outcome_index)
.ok_or(Error::UnknownOutcome)?;
let adaptor_sig = musig2::adaptor::aggregate_partial_signatures(
funding_spend_info.key_agg_ctx(),
aggnonce,
*attestation_lock_point,
partial_sigs,
sighash,
)?;
signatures
.outcome_tx_signatures
.insert(outcome_index, adaptor_sig);
}
Outcome::Expiry => {
let signature: CompactSignature = musig2::aggregate_partial_signatures(
funding_spend_info.key_agg_ctx(),
aggnonce,
partial_sigs,
sighash,
)?;
signatures.expiry_tx_signature = Some(signature);
}
};
}
Ok(signatures)
}
/// Verify the set of complete aggregated signatures on the
/// outcome and expiry transactions.
pub(crate) fn verify_outcome_tx_aggregated_signatures(
params: &ContractParameters,
our_pubkey: Point,
outcome_build_out: &OutcomeTransactionBuildOutput,
outcome_tx_signatures: &BTreeMap<OutcomeIndex, AdaptorSignature>,
expiry_tx_signature: Option<CompactSignature>,
) -> Result<(), Error> {
let funding_spend_info = &outcome_build_out.funding_spend_info;
let joint_pubkey: Point = funding_spend_info.key_agg_ctx().aggregated_pubkey();
// We only need to verify signatures on outcomes where our pubkey might
// win something.
let relevant_outcomes: BTreeSet<Outcome> = params
.win_conditions_claimable_by_pubkey(our_pubkey)
.ok_or(Error::InvalidKey)?
.into_iter()
.map(|win_cond| win_cond.outcome)
.collect();
// Construct a batch for efficient mass signature verification.
let batch: Vec<BatchVerificationRow> = relevant_outcomes
.into_iter()
.map(|outcome| {
let outcome_tx = outcome_build_out
.outcome_txs
.get(&outcome)
.ok_or(Error::UnknownOutcome)?;
let sighash = outcome_build_out
.funding_spend_info
.sighash_tx_outcome(outcome_tx)?;
let batch_row = match outcome {
// One adaptor signature for each possible attestation outcome.
Outcome::Attestation(outcome_index) => {
let adaptor_point = params
.event
.locking_points
.get(outcome_index)
.ok_or(Error::UnknownOutcome)?;
let &signature = outcome_tx_signatures.get(&outcome_index).ok_or(
Error::MissingSignature(String::from(
"outcome index outcome tx signatures",
)),
)?;
BatchVerificationRow::from_adaptor_signature(
joint_pubkey,
sighash,
signature,
*adaptor_point,
)
}
// One signature for the optional expiry transaction.
Outcome::Expiry => {
let signature = expiry_tx_signature
.ok_or(Error::MissingSignature(String::from("expiry tx signature")))?
.lift_nonce()?;
BatchVerificationRow::from_signature(joint_pubkey, sighash, signature)
}
};
Ok(batch_row)
})
.collect::<Result<_, Error>>()?;
// Verify all outcome signatures at once.
musig2::verify_batch(&batch)?;
Ok(())
}
/// Construct an input to spend an outcome transaction for a specific outcome.
/// Also returns a reference to the outcome TX's output so it can be used
/// to construct a set of [`bitcoin::sighash::Prevouts`].
pub(crate) fn outcome_tx_prevout<'x>(
outcome_build_out: &'x OutcomeTransactionBuildOutput,
outcome: &Outcome,
block_delay: u16,
) -> Result<(TxIn, &'x TxOut), Error> {
let outcome_tx = outcome_build_out
.outcome_txs()
.get(outcome)
.ok_or(Error::UnknownOutcome)?;
let outcome_input = TxIn {
previous_output: OutPoint {
txid: outcome_tx.compute_txid(),
vout: 0,
},
sequence: Sequence::from_height(block_delay),
..TxIn::default()
};
let prevout = outcome_tx
.output
.get(0)
.ok_or(Error::InvalidInput("missing outcome tx output"))?;
Ok((outcome_input, prevout))
}