add expiry outcome branch

This commit is contained in:
conduition
2024-02-21 06:13:31 +00:00
parent 1efc577244
commit cd681d0874
3 changed files with 229 additions and 140 deletions

View File

@@ -45,15 +45,13 @@ pub struct ContractParameters {
/// The event whose outcome determines the payouts.
pub event: EventAnnouncment,
/// An ordered list of payout under different outcomes. Should align with
/// `self.event.outcome_messages`.
pub outcome_payouts: Vec<PayoutWeights>,
/// Who is paid out in the event of an expiry (when the oracle attestation is not
/// received by [`event.expiry`][EventAnnouncment::expiry]). If this field is `None`,
/// then there is no expiry condition, and the money simply remains locked in the
/// funding outpoint until the Oracle's attestation is found.
pub expiry_payout: Option<PayoutWeights>,
/// A mapping of payout weights under different outcomes. Attestation indexes should
/// align with [`self.event.outcome_messages`][EventAnnouncment::outcome_messages].
///
/// If this map does not contain a key of [`Outcome::Expiry`], then there is no expiry
/// condition, and the money simply remains locked in the funding outpoint until the
/// Oracle's attestation is found.
pub outcome_payouts: BTreeMap<Outcome, PayoutWeights>,
/// A default mining fee rate to be used for pre-signed transactions.
pub fee_rate: FeeRate,
@@ -74,10 +72,22 @@ pub struct ContractParameters {
pub relative_locktime_block_delta: u16,
}
/// Represents one possible outcome branch of the DLC. This includes both
/// outcomes attested-to by the Oracle, and expiry.
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub enum Outcome {
/// Indicates the oracle attested to a particular outcome of the given index.
Attestation(usize),
/// Indicates the oracle failed to attest to any outcome, and the event expiry
/// timelock was reached.
Expiry,
}
/// Points to a situation where a player wins a payout from the DLC.
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
pub struct WinCondition {
pub outcome_index: usize,
pub outcome: Outcome,
pub winner: Player,
}
@@ -89,12 +99,29 @@ impl ContractParameters {
Ok(outcome_value)
}
/// Returns the set of players which this pubkey can sign for.
///
/// This might contain multiple players if the same key joined the DLC
/// with different ticket/payout hashes.
pub fn players_controlled_by_pubkey<'a>(&'a self, pubkey: Point) -> BTreeSet<&'a Player> {
self.players
.iter()
.filter(|player| player.pubkey == pubkey)
.collect()
}
/// Return the set of all win conditions which the given pubkey will need to sign
/// split transactions for.
///
/// If `pubkey` belongs to one or more players, this returns all [`WinCondition`]s
/// for outcomes in which the player or players are winners.
///
/// If `pubkey` belongs to the market maker, this returns every [`WinCondition`]
/// across the entire contract.
///
/// Returns `None` if the pubkey does not belong to any player in the DLC.
///
/// Returns an empty `BTreeSet if the player is part of the DLC, but isn't due to
/// Returns an empty `BTreeSet` if the player is part of the DLC, but isn't due to
/// receive any payouts on any DLC outcome.
pub fn win_conditions_controlled_by_pubkey(
&self,
@@ -103,13 +130,7 @@ impl ContractParameters {
// To sign as the market maker, the caller need only provide the correct secret key.
let is_market_maker = pubkey == self.market_maker.pubkey;
// This might contain multiple players if the same key joined the DLC
// with different ticket/payout hashes.
let controlling_players: BTreeSet<&Player> = self
.players
.iter()
.filter(|player| player.pubkey == pubkey)
.collect();
let controlling_players = self.players_controlled_by_pubkey(pubkey);
// Short circuit if this pubkey is not known.
if controlling_players.is_empty() && !is_market_maker {
@@ -117,17 +138,14 @@ impl ContractParameters {
}
let mut win_conditions_to_sign = BTreeSet::<WinCondition>::new();
for (outcome_index, payout_map) in self.outcome_payouts.iter().enumerate() {
for (&outcome, payout_map) in self.outcome_payouts.iter() {
// We want to sign the split TX for any win-conditions whose player is controlled
// by `pubkey`. If we're the market maker, we sign every win condition.
win_conditions_to_sign.extend(
payout_map
.keys()
.filter(|winner| is_market_maker || controlling_players.contains(winner))
.map(|&winner| WinCondition {
winner,
outcome_index,
}),
.map(|&winner| WinCondition { winner, outcome }),
);
}

View File

@@ -1,9 +1,11 @@
use bitcoin::{absolute::LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut};
use musig2::{AdaptorSignature, AggNonce, PartialSignature, PubNonce, SecNonce};
use musig2::{AdaptorSignature, AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce};
use secp::Scalar;
use std::collections::BTreeMap;
use crate::{
contract::ContractParameters,
contract::{ContractParameters, Outcome},
errors::Error,
parties::Player,
spend_info::{FundingSpendInfo, OutcomeSpendInfo},
@@ -13,26 +15,28 @@ use crate::{
/// This contains cached data used for constructing further transactions,
/// or signing the outcome transactions themselves.
pub(crate) struct OutcomeTransactionBuildOutput {
outcome_txs: Vec<Transaction>,
outcome_spend_infos: Vec<OutcomeSpendInfo>,
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) -> &[Transaction] {
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) -> &[OutcomeSpendInfo] {
pub(crate) fn outcome_spend_infos(&self) -> &BTreeMap<Outcome, 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`].
/// in the DLC (and the market maker).
///
/// If the contract has an expiry payout condition, this is simply the number
/// of outcomes plus one. Otherwise it is the number of outcomes exactly.
pub(crate) fn signatures_required(&self) -> usize {
self.outcome_txs.len()
}
@@ -50,34 +54,42 @@ pub(crate) fn build_outcome_txs(
};
let outcome_value = params.outcome_output_value()?;
let n_outcomes = params.event.outcome_messages.len();
let outcome_spend_infos: Vec<OutcomeSpendInfo> = (0..n_outcomes)
.map(|outcome_index| {
let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?;
let outcome_spend_infos: BTreeMap<Outcome, OutcomeSpendInfo> = params
.outcome_payouts
.iter()
.map(|(&outcome, payout_map)| {
let winners = payout_map.keys().copied();
OutcomeSpendInfo::new(
let spend_info = OutcomeSpendInfo::new(
winners,
&params.market_maker,
outcome_value,
params.relative_locktime_block_delta,
)
)?;
Ok((outcome, spend_info))
})
.collect::<Result<_, Error>>()?;
let outcome_txs: Vec<Transaction> = outcome_spend_infos
let outcome_txs: BTreeMap<Outcome, Transaction> = outcome_spend_infos
.iter()
.map(|outcome_spend_info| {
.map(|(&outcome, outcome_spend_info)| {
let outcome_output = TxOut {
value: outcome_value,
script_pubkey: outcome_spend_info.script_pubkey(),
};
Transaction {
let lock_time = match outcome {
Outcome::Expiry => LockTime::from_consensus(params.event.expiry),
Outcome::Attestation(_) => LockTime::ZERO, // Normal outcome transaction
};
let outcome_tx = Transaction {
version: bitcoin::transaction::Version::TWO,
lock_time: LockTime::ZERO,
lock_time,
input: vec![funding_input.clone()],
output: vec![outcome_output],
}
};
(outcome, outcome_tx)
})
.collect();
@@ -103,7 +115,7 @@ pub(crate) fn partial_sign_outcome_txs<'a>(
seckey: Scalar,
secnonces: impl IntoIterator<Item = SecNonce>,
aggnonces: impl IntoIterator<Item = &'a AggNonce>,
) -> Result<Vec<PartialSignature>, Error> {
) -> Result<BTreeMap<Outcome, PartialSignature>, Error> {
let outcome_txs = &outcome_build_out.outcome_txs;
let funding_spend_info = &outcome_build_out.funding_spend_info;
@@ -113,36 +125,47 @@ pub(crate) fn partial_sign_outcome_txs<'a>(
.pubkey_index(seckey.base_point_mul())
.ok_or(Error)?;
let n_outcomes = params.event.outcome_messages.len();
let mut outcome_partial_sigs = Vec::with_capacity(n_outcomes);
let mut outcome_partial_sigs = BTreeMap::<Outcome, PartialSignature>::new();
let mut aggnonce_iter = aggnonces.into_iter();
let mut secnonce_iter = secnonces.into_iter();
for (outcome_index, outcome_tx) in outcome_txs.into_iter().enumerate() {
for (&outcome, outcome_tx) in outcome_txs {
let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces
let secnonce = secnonce_iter.next().ok_or(Error)?; // must provide enough secnonces
// All outcome TX signatures should be locked by the oracle's outcome point.
let attestation_lock_point = params
.event
.attestation_lock_point(outcome_index)
.ok_or(Error)?;
// Hash the outcome TX.
let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?;
// partially sign the sighash.
let partial_sig = musig2::adaptor::sign_partial(
funding_spend_info.key_agg_ctx(),
seckey,
secnonce,
aggnonce,
attestation_lock_point,
sighash,
)?;
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
.attestation_lock_point(outcome_index)
.ok_or(Error)?;
outcome_partial_sigs.push(partial_sig);
// 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)
}
@@ -166,34 +189,63 @@ pub(crate) fn verify_outcome_tx_partial_signatures<'p, 'a>(
let mut pubnonce_iter = pubnonces.into_iter();
let mut partial_sig_iter = partial_signatures.into_iter();
for (outcome_index, outcome_tx) in outcome_txs.into_iter().enumerate() {
for (&outcome, outcome_tx) in outcome_txs {
let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces
let pubnonce = pubnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces
let pubnonce = pubnonce_iter.next().ok_or(Error)?; // must provide enough pubnonces
let partial_sig = partial_sig_iter.next().ok_or(Error)?; // must provide enough sigs
// Hash the outcome TX.
let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?;
// All outcome TX signatures should be locked by the oracle's outcome point.
let attestation_lock_point = params
.event
.attestation_lock_point(outcome_index)
.ok_or(Error)?;
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
.attestation_lock_point(outcome_index)
.ok_or(Error)?;
musig2::adaptor::verify_partial(
funding_spend_info.key_agg_ctx(),
partial_sig,
aggnonce,
attestation_lock_point,
player.pubkey,
pubnonce,
sighash,
)?;
musig2::adaptor::verify_partial(
funding_spend_info.key_agg_ctx(),
partial_sig,
aggnonce,
attestation_lock_point,
player.pubkey,
pubnonce,
sighash,
)?;
}
Outcome::Expiry => {
musig2::verify_partial(
funding_spend_info.key_agg_ctx(),
partial_sig,
aggnonce,
player.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) adaptor_signatures: Vec<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
@@ -208,7 +260,7 @@ pub(crate) fn aggregate_outcome_tx_adaptor_signatures<'a, S>(
outcome_build_out: &OutcomeTransactionBuildOutput,
aggnonces: impl IntoIterator<Item = &'a AggNonce>,
partial_signature_groups: impl IntoIterator<Item = S>,
) -> Result<Vec<AdaptorSignature>, Error>
) -> Result<OutcomeSignatures, Error>
where
S: IntoIterator<Item = PartialSignature>,
{
@@ -218,34 +270,52 @@ where
let mut aggnonce_iter = aggnonces.into_iter();
let mut partial_sig_group_iter = partial_signature_groups.into_iter();
outcome_txs
.into_iter()
.enumerate()
.map(|(outcome_index, outcome_tx)| {
// must provide a set of sigs for each TX
let partial_sigs = partial_sig_group_iter.next().ok_or(Error)?;
let mut signatures = OutcomeSignatures {
adaptor_signatures: Vec::with_capacity(params.event.outcome_messages.len()),
expiry_tx_signature: None,
};
let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces
for (&outcome, outcome_tx) in outcome_txs {
// must provide a set of sigs for each TX
let partial_sigs = partial_sig_group_iter.next().ok_or(Error)?;
let attestation_lock_point = params
.event
.attestation_lock_point(outcome_index)
.ok_or(Error)?;
let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces
// Hash the outcome TX.
let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?;
// Hash the outcome TX.
let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?;
let adaptor_sig = musig2::adaptor::aggregate_partial_signatures(
funding_spend_info.key_agg_ctx(),
aggnonce,
attestation_lock_point,
partial_sigs,
sighash,
)?;
match outcome {
Outcome::Attestation(outcome_index) => {
let attestation_lock_point = params
.event
.attestation_lock_point(outcome_index)
.ok_or(Error)?;
Ok(adaptor_sig)
})
.collect()
let adaptor_sig = musig2::adaptor::aggregate_partial_signatures(
funding_spend_info.key_agg_ctx(),
aggnonce,
attestation_lock_point,
partial_sigs,
sighash,
)?;
signatures.adaptor_signatures.push(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)
}
/// Construct an input to spend an outcome transaction for a specific outcome.
@@ -253,13 +323,10 @@ where
/// to construct a set of [`bitcoin::sighash::Prevouts`].
pub(crate) fn outcome_tx_prevout<'x>(
outcome_build_out: &'x OutcomeTransactionBuildOutput,
outcome_index: usize,
outcome: &Outcome,
block_delay: u16,
) -> Result<(TxIn, &'x TxOut), Error> {
let outcome_tx = outcome_build_out
.outcome_txs()
.get(outcome_index)
.ok_or(Error)?;
let outcome_tx = outcome_build_out.outcome_txs().get(outcome).ok_or(Error)?;
let outcome_input = TxIn {
previous_output: OutPoint {
@@ -270,7 +337,7 @@ pub(crate) fn outcome_tx_prevout<'x>(
..TxIn::default()
};
let prevout = outcome_tx.output.get(outcome_index).ok_or(Error)?;
let prevout = outcome_tx.output.get(0).ok_or(Error)?;
Ok((outcome_input, prevout))
}

View File

@@ -5,7 +5,7 @@ use secp::Scalar;
use crate::{
consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE},
contract::{self, fees, outcome::OutcomeTransactionBuildOutput},
contract::{ContractParameters, WinCondition},
contract::{ContractParameters, Outcome, WinCondition},
errors::Error,
parties::Player,
spend_info::SplitSpendInfo,
@@ -17,14 +17,14 @@ use std::{borrow::Borrow, collections::BTreeMap};
/// This contains cached data used for constructing further transactions,
/// or signing the split transactions themselves.
pub(crate) struct SplitTransactionBuildOutput {
split_txs: Vec<Transaction>,
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) -> &[Transaction] {
pub(crate) fn split_txs(&self) -> &BTreeMap<Outcome, Transaction> {
&self.split_txs
}
@@ -43,16 +43,13 @@ pub(crate) fn build_split_txs(
params: &ContractParameters,
outcome_build_output: &OutcomeTransactionBuildOutput,
) -> Result<SplitTransactionBuildOutput, Error> {
let n_outcomes = params.outcome_payouts.len();
let mut split_spend_infos = BTreeMap::<WinCondition, SplitSpendInfo>::new();
let mut split_txs = Vec::<Transaction>::with_capacity(n_outcomes);
for outcome_index in 0..params.outcome_payouts.len() {
let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?;
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_index)
.get(&outcome)
.ok_or(Error)?;
// Fee estimation
@@ -69,7 +66,7 @@ pub(crate) fn build_split_txs(
let (outcome_input, _) = contract::outcome::outcome_tx_prevout(
outcome_build_output,
outcome_index,
&outcome,
params.relative_locktime_block_delta, // Split TXs have 1*delta block delay
)?;
@@ -90,17 +87,19 @@ pub(crate) fn build_split_txs(
let win_cond = WinCondition {
winner: player,
outcome_index,
outcome,
};
split_spend_infos.insert(win_cond, split_spend_info);
}
split_txs.push(Transaction {
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 {
@@ -138,23 +137,24 @@ pub(crate) fn partial_sign_split_txs<'a>(
return Ok(partial_signatures);
}
let split_txs = &split_build_out.split_txs;
let mut aggnonce_iter = aggnonces.into_iter();
let mut secnonce_iter = secnonces.into_iter();
for win_cond in win_conditions_to_sign {
let split_tx = split_txs.get(win_cond.outcome_index).ok_or(Error)?;
let split_tx = split_build_out
.split_txs()
.get(&win_cond.outcome)
.ok_or(Error)?;
let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces
let secnonce = secnonce_iter.next().ok_or(Error)?; // must provide enough secnonces
// Hash the split TX.
let outcome_spend_info = outcome_build_out
.outcome_spend_infos()
.get(win_cond.outcome_index)
.get(&win_cond.outcome)
.ok_or(Error)?;
// Hash the split TX.
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?;
// Partially sign the sighash.
@@ -193,10 +193,11 @@ pub(crate) fn verify_split_tx_partial_signatures(
.win_conditions_controlled_by_pubkey(player.pubkey)
.ok_or(Error)?;
let split_txs = &split_build_out.split_txs;
for win_cond in win_conditions_to_sign {
let split_tx = split_txs.get(win_cond.outcome_index).ok_or(Error)?;
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
@@ -204,7 +205,7 @@ pub(crate) fn verify_split_tx_partial_signatures(
let outcome_spend_info = outcome_build_out
.outcome_spend_infos()
.get(win_cond.outcome_index)
.get(&win_cond.outcome)
.ok_or(Error)?;
// Hash the split TX.
@@ -240,13 +241,14 @@ where
&'s S: IntoIterator<Item = P>,
P: Borrow<PartialSignature>,
{
let split_txs = &split_build_out.split_txs;
split_build_out
.split_spend_infos
.keys()
.map(|&win_cond| {
let split_tx = split_txs.get(win_cond.outcome_index).ok_or(Error)?;
let split_tx = split_build_out
.split_txs()
.get(&win_cond.outcome)
.ok_or(Error)?;
let relevant_partial_sigs = partial_signatures_by_win_cond
.get(&win_cond)
@@ -258,7 +260,7 @@ where
let outcome_spend_info = outcome_build_out
.outcome_spend_infos()
.get(win_cond.outcome_index)
.get(&win_cond.outcome)
.ok_or(Error)?;
// Hash the split TX.
@@ -282,17 +284,19 @@ where
pub(crate) fn split_tx_prevout<'x>(
params: &ContractParameters,
split_build_out: &'x SplitTransactionBuildOutput,
outcome_index: usize,
winner: &Player,
win_cond: &WinCondition,
block_delay: u16,
) -> Result<(TxIn, &'x TxOut), Error> {
let split_tx = split_build_out
.split_txs()
.get(outcome_index)
.get(&win_cond.outcome)
.ok_or(Error)?;
let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?;
let split_tx_output_index = payout_map.keys().position(|p| p == winner).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(|p| p == &win_cond.winner)
.ok_or(Error)?;
let input = TxIn {
previous_output: OutPoint {