diff --git a/src/consts.rs b/src/consts.rs new file mode 100644 index 0000000..2d7feb2 --- /dev/null +++ b/src/consts.rs @@ -0,0 +1,19 @@ +/// The serialized length of a P2TR script pubkey. +pub const P2TR_SCRIPT_PUBKEY_SIZE: usize = 34; + +/// This was computed using [`bitcoin`] v0.31.1. +/// Test coverage ensures this stays is up-to-date. +pub const P2TR_DUST_VALUE: bitcoin::Amount = bitcoin::Amount::from_sat(330); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_p2tr_dust() { + let xonly = bitcoin::XOnlyPublicKey::from_slice(&[1; 32]).unwrap(); + let tweaked = bitcoin::key::TweakedPublicKey::dangerous_assume_tweaked(xonly); + let script = bitcoin::ScriptBuf::new_p2tr_tweaked(tweaked); + assert_eq!(script.dust_value(), P2TR_DUST_VALUE); + } +} diff --git a/src/contract/fees.rs b/src/contract/fees.rs new file mode 100644 index 0000000..99bfec2 --- /dev/null +++ b/src/contract/fees.rs @@ -0,0 +1,32 @@ +use bitcoin::{transaction::InputWeightPrediction, Amount, FeeRate}; + +use crate::errors::Error; + +pub(crate) fn fee_calc_safe( + fee_rate: FeeRate, + input_weights: I, + output_spk_lens: O, +) -> Result +where + I: IntoIterator, + O: IntoIterator, +{ + let tx_weight = bitcoin::transaction::predict_weight(input_weights, output_spk_lens); + let fee = fee_rate.fee_wu(tx_weight).ok_or(Error)?; + Ok(fee) +} + +pub(crate) fn fee_subtract_safe( + available_coins: Amount, + fee: Amount, + dust_threshold: Amount, +) -> Result { + if fee >= available_coins { + return Err(Error); + } + let after_fee = available_coins.checked_sub(fee).ok_or(Error)?; + if after_fee <= dust_threshold { + return Err(Error); + } + Ok(after_fee) +} diff --git a/src/contract/mod.rs b/src/contract/mod.rs new file mode 100644 index 0000000..d2eeb4b --- /dev/null +++ b/src/contract/mod.rs @@ -0,0 +1,6 @@ +mod parameters; + +pub mod fees; +pub mod outcome; +pub mod split; +pub use parameters::*; diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs new file mode 100644 index 0000000..d3c1cb0 --- /dev/null +++ b/src/contract/outcome.rs @@ -0,0 +1,211 @@ +use bitcoin::{absolute::LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut}; +use musig2::{AdaptorSignature, AggNonce, PartialSignature, PubNonce, SecNonce}; +use secp::Scalar; + +use crate::{ + contract::ContractParameters, + errors::Error, + parties::Player, + spend_info::{FundingSpendInfo, OutcomeSpendInfo}, +}; + +pub(crate) struct OutcomeTransactionBuildOutput { + pub(crate) outcome_txs: Vec, + pub(crate) outcome_spend_infos: Vec, + funding_spend_info: FundingSpendInfo, +} + +/// 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 { + let funding_input = TxIn { + previous_output: funding_outpoint, + sequence: Sequence::MAX, + ..TxIn::default() + }; + let outcome_value = params.outcome_output_value()?; + + let n_outcomes = params.event.outcome_messages.len(); + let outcome_spend_infos: Vec = (0..n_outcomes) + .map(|outcome_index| { + let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?; + let winners = payout_map.keys().copied(); + + OutcomeSpendInfo::new( + winners, + ¶ms.market_maker, + outcome_value, + params.relative_locktime_block_delta, + ) + }) + .collect::>()?; + + let outcome_txs: Vec = outcome_spend_infos + .iter() + .map(|outcome_spend_info| { + let outcome_output = TxOut { + value: outcome_value, + script_pubkey: outcome_spend_info.script_pubkey(), + }; + Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: LockTime::ZERO, + input: vec![funding_input.clone()], + output: vec![outcome_output], + } + }) + .collect(); + + let funding_spend_info = + FundingSpendInfo::new(¶ms.market_maker, ¶ms.players, params.funding_value)?; + + let output = OutcomeTransactionBuildOutput { + outcome_txs, + outcome_spend_infos, + funding_spend_info, + }; + + Ok(output) +} + +pub(crate) fn partial_sign_outcome_txs<'a>( + params: &ContractParameters, + outcome_build_out: &OutcomeTransactionBuildOutput, + seckey: impl Into, + secnonces: impl IntoIterator, + aggnonces: impl IntoIterator, +) -> Result, 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. + let seckey = seckey.into(); + funding_spend_info + .key_agg_ctx() + .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 aggnonce_iter = aggnonces.into_iter(); + let mut secnonce_iter = secnonces.into_iter(); + + for (outcome_index, outcome_tx) in outcome_txs.into_iter().enumerate() { + 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 outcome_lock_point = params + .event + .outcome_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, + outcome_lock_point, + sighash, + )?; + + outcome_partial_sigs.push(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<'p, 'a>( + params: &ContractParameters, + outcome_build_out: &OutcomeTransactionBuildOutput, + player: &Player, + pubnonces: impl IntoIterator, + aggnonces: impl IntoIterator, + partial_signatures: impl IntoIterator, +) -> Result<(), Error> { + let outcome_txs = &outcome_build_out.outcome_txs; + let funding_spend_info = &outcome_build_out.funding_spend_info; + + let mut aggnonce_iter = aggnonces.into_iter(); + 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() { + 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 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 outcome_lock_point = params + .event + .outcome_lock_point(outcome_index) + .ok_or(Error)?; + + musig2::adaptor::verify_partial( + funding_spend_info.key_agg_ctx(), + partial_sig, + aggnonce, + outcome_lock_point, + player.pubkey, + pubnonce, + sighash, + )?; + } + + Ok(()) +} + +pub(crate) fn aggregate_outcome_tx_adaptor_signatures<'a, S>( + params: &ContractParameters, + outcome_build_out: &OutcomeTransactionBuildOutput, + aggnonces: impl IntoIterator, + partial_signature_groups: impl IntoIterator, +) -> Result, Error> +where + S: IntoIterator, +{ + let outcome_txs = &outcome_build_out.outcome_txs; + let funding_spend_info = &outcome_build_out.funding_spend_info; + + 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 aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces + + let outcome_lock_point = params + .event + .outcome_lock_point(outcome_index) + .ok_or(Error)?; + + // 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, + outcome_lock_point, + partial_sigs, + sighash, + )?; + + Ok(adaptor_sig) + }) + .collect() +} diff --git a/src/contract/parameters.rs b/src/contract/parameters.rs new file mode 100644 index 0000000..b54751a --- /dev/null +++ b/src/contract/parameters.rs @@ -0,0 +1,105 @@ +use bitcoin::{Amount, FeeRate}; +use secp::Point; + +use crate::{ + consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE}, + contract::fees, + errors::Error, + oracles::EventAnnouncment, + parties::{MarketMaker, Player}, +}; + +use std::collections::{BTreeMap, BTreeSet}; + +/// Represents a mapping of player to payout weight for a given outcome. +/// +/// A player's payout is proportional to the size of their payout weight +/// in comparison to the payout weights of all other winners. +pub type PayoutWeights = BTreeMap; + +#[derive(Debug, Clone)] +pub struct ContractParameters { + /// The market maker who provides capital for the DLC ticketing process. + pub market_maker: MarketMaker, + + /// Players in the DLC. + pub players: Vec, + + /// 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, + + /// Who is paid out in the event of an expiry. + pub expiry_payout: Option, + + /// A default mining fee rate to be used for pre-signed transactions. + pub fee_rate: FeeRate, + + /// The amount of on-chain capital which the market maker will provide when funding + /// the initial multisig deposit contract. + pub funding_value: Amount, + + /// A reasonable number of blocks within which a transaction can confirm. + /// Used for enforcing relative locktime timeout spending conditions. + pub relative_locktime_block_delta: u16, +} + +/// Points to a situation where a player wins a payout from the DLC. +#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)] +pub(crate) struct WinCondition { + pub(crate) outcome_index: usize, + pub(crate) winner: Player, +} + +impl ContractParameters { + pub(crate) fn outcome_output_value(&self) -> Result { + let input_weights = [bitcoin::transaction::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) + } + + /// Return the set of all win conditions which this pubkey will need to sign for. + /// + /// This might be empty if the player isn't due to receive any payouts on any DLC outcome. + pub(crate) fn win_conditions_controlled_by_pubkey( + &self, + pubkey: Point, + ) -> Option> { + // 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(); + + // Short circuit if this pubkey is not known. + if controlling_players.is_empty() && !is_market_maker { + return None; + } + + let mut win_conditions_to_sign = BTreeSet::::new(); + for (outcome_index, payout_map) in self.outcome_payouts.iter().enumerate() { + // We want to sign the split TX for any win-conditions whose player is controlled + // by `seckey`. 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, + }), + ); + } + + Some(win_conditions_to_sign) + } +} diff --git a/src/contract/split.rs b/src/contract/split.rs new file mode 100644 index 0000000..b89a17c --- /dev/null +++ b/src/contract/split.rs @@ -0,0 +1,254 @@ +use bitcoin::{absolute::LockTime, OutPoint, Sequence, Transaction, TxIn, TxOut}; +use musig2::{AggNonce, CompactSignature, PartialSignature, PubNonce, SecNonce}; +use secp::Scalar; + +use crate::{ + consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE}, + contract::{fees, outcome::OutcomeTransactionBuildOutput}, + contract::{ContractParameters, WinCondition}, + errors::Error, + parties::Player, + spend_info::SplitSpendInfo, +}; + +use std::{borrow::Borrow, collections::BTreeMap}; + +pub(crate) struct SplitTransactionBuildOutput { + split_txs: Vec, + split_spend_infos: BTreeMap, +} + +/// Build the set of split transactions which splits payouts 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 { + let outcome_txs = &outcome_build_output.outcome_txs; + + let mut split_spend_infos = BTreeMap::::new(); + let mut split_txs = Vec::::with_capacity(outcome_txs.len()); + + for (outcome_index, outcome_tx) in outcome_txs.into_iter().enumerate() { + let payout_map = params.outcome_payouts.get(outcome_index).ok_or(Error)?; + + let outcome_spend_info = &outcome_build_output + .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 outcome_input = TxIn { + previous_output: OutPoint { + txid: outcome_tx.txid(), + vout: 0, + }, + // Split TXs have 1*delta block delay + sequence: Sequence::from_height(params.relative_locktime_block_delta), + ..TxIn::default() + }; + + // payout_map 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)?; + + let split_spend_info = SplitSpendInfo::new( + player, + ¶ms.market_maker, + payout_value, + params.relative_locktime_block_delta, + )?; + + split_tx_outputs.push(TxOut { + value: payout_value, + script_pubkey: split_spend_info.script_pubkey(), + }); + + let win_cond = WinCondition { + winner: player, + outcome_index, + }; + split_spend_infos.insert(win_cond, split_spend_info); + } + + split_txs.push(Transaction { + version: bitcoin::transaction::Version::TWO, + lock_time: LockTime::ZERO, + input: vec![outcome_input], + output: split_tx_outputs, + }); + } + + 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<'a>( + params: &ContractParameters, + outcome_build_out: &OutcomeTransactionBuildOutput, + split_build_out: &SplitTransactionBuildOutput, + seckey: impl Into, + secnonces: impl IntoIterator, + aggnonces: impl IntoIterator, +) -> Result, Error> { + let seckey = seckey.into(); + let pubkey = seckey.base_point_mul(); + + let mut partial_signatures = BTreeMap::::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); + } + + 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 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) + .ok_or(Error)?; + + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + + // 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) +} + +pub(crate) fn verify_split_tx_partial_signatures( + params: &ContractParameters, + outcome_build_out: &OutcomeTransactionBuildOutput, + split_build_out: &SplitTransactionBuildOutput, + player: &Player, + pubnonces: &BTreeMap, + aggnonces: &BTreeMap, + partial_signatures: &BTreeMap, +) -> Result<(), Error> { + let win_conditions_to_sign = params + .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 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_index) + .ok_or(Error)?; + + // Hash the split TX. + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + + // 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, + player.pubkey, + pubnonce, + sighash, + )?; + } + + Ok(()) +} + +/// Aggregate all partial signatures on every spending path of all split transactions. +pub(crate) fn aggregate_split_tx_signatures<'s, S, P>( + outcome_build_out: &OutcomeTransactionBuildOutput, + split_build_out: &SplitTransactionBuildOutput, + aggnonces: &BTreeMap, + partial_signatures_by_win_cond: &'s BTreeMap, +) -> Result, Error> +where + &'s S: IntoIterator, + P: Borrow, +{ + 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 relevant_partial_sigs = partial_signatures_by_win_cond + .get(&win_cond) + .ok_or(Error)? + .into_iter() + .map(|sig| sig.borrow().clone()); + + let aggnonce = aggnonces.get(&win_cond).ok_or(Error)?; + + let outcome_spend_info = outcome_build_out + .outcome_spend_infos + .get(win_cond.outcome_index) + .ok_or(Error)?; + + // Hash the split TX. + let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?; + + 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() +} diff --git a/src/errors.rs b/src/errors.rs index ce7542c..58d21ca 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -22,6 +22,12 @@ impl From for Error { } } +impl From for Error { + fn from(_: musig2::errors::VerifyError) -> Self { + Error + } +} + impl From for Error { fn from(_: musig2::errors::SigningError) -> Self { Error diff --git a/src/hashlock.rs b/src/hashlock.rs new file mode 100644 index 0000000..b63173a --- /dev/null +++ b/src/hashlock.rs @@ -0,0 +1,29 @@ +use sha2::Digest as _; + +/// The size for ticket preimages. +pub const PREIMAGE_SIZE: usize = 32; + +/// Compute the SHA256 hash of some input data. +pub fn sha256(input: &[u8]) -> [u8; 32] { + sha2::Sha256::new().chain_update(input).finalize().into() +} + +/// A handy type-alias for ticket and payout preimages. +/// +/// We use random 32 byte preimages for compatibility with +/// lightning network clients. +pub type Preimage = [u8; PREIMAGE_SIZE]; + +/// Generate a random [`Preimage`] from a secure RNG. +pub fn preimage_random(rng: &mut R) -> Preimage { + let mut preimage = [0u8; PREIMAGE_SIZE]; + rng.fill_bytes(&mut preimage); + preimage +} + +/// Parse a preimage from a hex string. +pub fn preimage_from_hex(s: &str) -> Result { + let mut preimage = [0u8; PREIMAGE_SIZE]; + hex::decode_to_slice(s, &mut preimage)?; + Ok(preimage) +} diff --git a/src/lib.rs b/src/lib.rs index 3d16c42..05e9f6a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,1494 +1,7 @@ -mod errors; -mod oracles; - -// external -use bitcoin::{ - absolute::LockTime, - key::constants::SCHNORR_SIGNATURE_SIZE, - opcodes::all::*, - script::ScriptBuf, - sighash::{Prevouts, SighashCache, TapSighashType}, - taproot::{ - LeafVersion, TapLeafHash, TaprootSpendInfo, TAPROOT_CONTROL_BASE_SIZE, - TAPROOT_CONTROL_NODE_SIZE, - }, - transaction::InputWeightPrediction, - Amount, FeeRate, OutPoint, Sequence, TapSighash, Transaction, TxIn, TxOut, -}; -use musig2::{AggNonce, KeyAggContext, PartialSignature, SecNonce}; -use secp::{Point, Scalar}; -use sha2::Digest as _; - -// stdlib -use std::collections::{BTreeMap, BTreeSet}; - -// crate -use errors::Error; -use oracles::EventAnnouncment; - -/// The serialized length of a P2TR script pubkey. -const P2TR_SCRIPT_PUBKEY_SIZE: usize = 34; - -/// This was computed using [`bitcoin`] v0.31.1. -/// Test coverage ensures this stays is up-to-date. -const P2TR_DUST_VALUE: Amount = Amount::from_sat(330); - -/// The agent who provides the on-chain capital to facilitate the ticketed DLC. -/// Could be one of the players in the DLC, or could be a neutral 3rd party -/// who wishes to profit by leveraging their capital. -#[derive(Debug, Clone)] -pub struct MarketMaker { - pub pubkey: Point, -} - -/// Compute the SHA256 hash of some input data. -pub fn sha256(input: &[u8]) -> [u8; 32] { - sha2::Sha256::new().chain_update(input).finalize().into() -} - -/// The size for ticket preimages. -pub const PREIMAGE_SIZE: usize = 32; - -/// A handy type-alias for ticket and payout preimages. -/// -/// We use random 32 byte preimages for compatibility with -/// lightning network clients. -pub type Preimage = [u8; PREIMAGE_SIZE]; - -/// Generate a random [`Preimage`] from a secure RNG. -pub fn preimage_random(rng: &mut R) -> Preimage { - let mut preimage = [0u8; PREIMAGE_SIZE]; - rng.fill_bytes(&mut preimage); - preimage -} - -/// Parse a preimage from a hex string. -pub fn preimage_from_hex(s: &str) -> Result { - let mut preimage = [0u8; PREIMAGE_SIZE]; - hex::decode_to_slice(s, &mut preimage)?; - Ok(preimage) -} - -/// A player in a ticketed DLC. Each player is identified by a public key, -/// but also by their ticket hash. If a player can learn the preimage of -/// their ticket hash (usually by purchasing it via Lightning), they can -/// claim winnings from DLC outcomes. -/// -/// The same pubkey can participate in the same ticketed DLC under different -/// ticket hashes, so players might share common pubkeys. However, for the -/// economics of the contract to work, every player should be allocated -/// their own completely unique ticket hash. -#[derive(Debug, Clone, Copy, Ord, PartialOrd, Hash, Eq, PartialEq)] -pub struct Player { - /// A public key controlled by the player. - pub pubkey: Point, - - /// The ticket hashes used for HTLCs. To buy into the DLC, players must - /// purchase the preimages of these hashes. - pub ticket_hash: [u8; 32], - - /// A hash used for unlocking the split TX output early. To allow winning - /// players to receive off-chain payouts, they must provide this `payout_hash`, - /// for which they know the preimage. By selling the preimage to the market maker, - /// they allow the market maker to reclaim the on-chain funds. - pub payout_hash: [u8; 32], -} - -#[derive(Debug, Clone)] -pub struct FundingSpendInfo { - key_agg_ctx: KeyAggContext, - funding_value: Amount, -} - -impl FundingSpendInfo { - fn new<'p>( - market_maker: &MarketMaker, - players: impl IntoIterator, - funding_value: Amount, - ) -> Result { - let mut pubkeys: Vec = players - .into_iter() - .map(|player| player.pubkey) - .chain([market_maker.pubkey]) - .collect(); - pubkeys.sort(); - - let key_agg_ctx = KeyAggContext::new(pubkeys)?; - - Ok(FundingSpendInfo { - key_agg_ctx, - funding_value, - }) - } - - /// Return a reference to the [`KeyAggContext`] used to spend the multisig funding output. - pub fn key_agg_ctx(&self) -> &KeyAggContext { - &self.key_agg_ctx - } - - /// Returns the TX locking script for funding the ticketed DLC multisig. - pub fn script_pubkey(&self) -> ScriptBuf { - ScriptBuf::new_p2tr( - secp256k1::SECP256K1, - self.key_agg_ctx.aggregated_pubkey(), - None, - ) - } - - /// Compute the signature hash for a given outcome transaction. - pub fn sighash_tx_outcome( - &self, - outcome_tx: &Transaction, - ) -> Result { - let funding_prevouts = [TxOut { - script_pubkey: self.script_pubkey(), - value: self.funding_value, - }]; - - SighashCache::new(outcome_tx).taproot_key_spend_signature_hash( - 0, - &Prevouts::All(&funding_prevouts), - TapSighashType::Default, - ) - } -} - -/// Represents a taproot contract which encodes spending conditions for -/// the given outcome index's outcome TX. This tree is meant to encumber joint -/// signatures on the split transaction. Any winning player should be able to -/// broadcast the split transaction, but only if they know their ticket preimage. -/// The market maker should be able to freely spend the money if no ticketholder -/// can publish the split TX before a timeout. -/// -/// Since we're using hashlocks and not PTLCs here, we unfortunately need a -/// tapscript leaf for every winner, and since tapscript signatures must commit -/// to the leaf, the winners must construct distinct musig2 signatures for each -/// leaf. This must be repeated for every outcome. With `n` outcomes and `w` -/// winners per outcome, we must create a total of `n * w` signatures. -/// -/// Once PTLCs are available, we can instead sign the split transaction once -/// and distribute adaptor-signatures to each player, encrypted under the -/// player's ticket point. -#[derive(Debug, Clone)] -pub struct OutcomeSpendInfo { - untweaked_ctx: KeyAggContext, - tweaked_ctx: KeyAggContext, - outcome_value: Amount, - spend_info: TaprootSpendInfo, - winner_split_scripts: BTreeMap, - reclaim_script: ScriptBuf, -} - -impl OutcomeSpendInfo { - fn new>( - winners: W, - market_maker: &MarketMaker, - outcome_value: Amount, - block_delta: u16, - ) -> Result { - let winners: Vec = winners.into_iter().collect(); - let mut pubkeys: Vec = [market_maker.pubkey] - .into_iter() - .chain(winners.iter().map(|w| w.pubkey)) - .collect(); - pubkeys.sort(); - let untweaked_ctx = KeyAggContext::new(pubkeys)?; - let joint_outcome_pubkey: Point = untweaked_ctx.aggregated_pubkey(); - - let winner_split_scripts: BTreeMap = winners - .iter() - .map(|&winner| { - // The winner split script, used by winning players to spend - // the outcome transaction using the split transaction. - // - // Input: - let script = bitcoin::script::Builder::new() - // Check ticket preimage: OP_SHA256 OP_EQUALVERIFY - .push_opcode(OP_SHA256) - .push_slice(winner.ticket_hash) - .push_opcode(OP_EQUALVERIFY) - // Check joint signature: OP_CHECKSIG - .push_slice(joint_outcome_pubkey.serialize_xonly()) - .push_opcode(OP_CHECKSIG) - // Don't need OP_CSV. - // Sequence number is enforced by multisig key: split TX is pre-signed. - .into_script(); - - (winner, script) - }) - .collect(); - - // The reclaim script, used by the market maker to recover their capital - // if none of the winning players bought their ticket preimages. - let reclaim_script = bitcoin::script::Builder::new() - // Check relative locktime: <2*delta> OP_CSV OP_DROP - .push_int(2 * block_delta as i64) - .push_opcode(OP_CSV) - .push_opcode(OP_DROP) - // Check signature from market maker: OP_CHECKSIG - .push_slice(market_maker.pubkey.serialize_xonly()) - .push_opcode(OP_CHECKSIG) - .into_script(); - - let weighted_script_leaves = winner_split_scripts - .values() - .cloned() - .map(|script| (1, script)) - .chain([(999999999, reclaim_script.clone())]); // reclaim script gets highest priority - - let tr_spend_info = TaprootSpendInfo::with_huffman_tree( - secp256k1::SECP256K1, - joint_outcome_pubkey.into(), - weighted_script_leaves, - )?; - - let tweaked_ctx = untweaked_ctx.clone().with_taproot_tweak( - tr_spend_info - .merkle_root() - .expect("should always have merkle root") - .as_ref(), - )?; - - let outcome_spend_info = OutcomeSpendInfo { - untweaked_ctx, - tweaked_ctx, - outcome_value, - spend_info: tr_spend_info, - winner_split_scripts, - reclaim_script, - }; - Ok(outcome_spend_info) - } - - /// Returns the TX locking script for this this outcome contract. - pub fn script_pubkey(&self) -> ScriptBuf { - ScriptBuf::new_p2tr_tweaked(self.spend_info.output_key()) - } - - /// Computes the input weight when spending the output of the outcome TX - /// as an input of the split TX. This assumes one of the winning ticketholders' - /// tapscript leaves is being used to build a witness. This prediction aims - /// for fee estimation in the worst-case-scenario: For the winner whose tapscript - /// leaf is deepest in the taptree (and hence requires the largest merkle proof). - pub fn input_weight_for_split_tx(&self) -> InputWeightPrediction { - let outcome_script_len = self - .winner_split_scripts - .values() - .nth(0) - .expect("always at least one winner") - .len(); - - let max_taptree_depth = self - .spend_info - .script_map() - .values() - .flatten() - .map(|proof| proof.len()) - .max() - .expect("always has at least one node"); - - // The witness stack for the split TX (spends the outcome TX) is: - //