refactor into multifile layout using functions

This commit is contained in:
conduition
2024-02-16 03:52:16 +00:00
parent 0fa8432641
commit bc606be476
14 changed files with 1233 additions and 1494 deletions

19
src/consts.rs Normal file
View File

@@ -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);
}
}

32
src/contract/fees.rs Normal file
View File

@@ -0,0 +1,32 @@
use bitcoin::{transaction::InputWeightPrediction, Amount, FeeRate};
use crate::errors::Error;
pub(crate) fn fee_calc_safe<I, O>(
fee_rate: FeeRate,
input_weights: I,
output_spk_lens: O,
) -> Result<Amount, Error>
where
I: IntoIterator<Item = InputWeightPrediction>,
O: IntoIterator<Item = usize>,
{
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<Amount, Error> {
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)
}

6
src/contract/mod.rs Normal file
View File

@@ -0,0 +1,6 @@
mod parameters;
pub mod fees;
pub mod outcome;
pub mod split;
pub use parameters::*;

211
src/contract/outcome.rs Normal file
View File

@@ -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<Transaction>,
pub(crate) outcome_spend_infos: Vec<OutcomeSpendInfo>,
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<OutcomeTransactionBuildOutput, Error> {
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<OutcomeSpendInfo> = (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,
&params.market_maker,
outcome_value,
params.relative_locktime_block_delta,
)
})
.collect::<Result<_, Error>>()?;
let outcome_txs: Vec<Transaction> = 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(&params.market_maker, &params.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<Scalar>,
secnonces: impl IntoIterator<Item = SecNonce>,
aggnonces: impl IntoIterator<Item = &'a AggNonce>,
) -> Result<Vec<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.
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<Item = &'p PubNonce>,
aggnonces: impl IntoIterator<Item = &'a AggNonce>,
partial_signatures: impl IntoIterator<Item = PartialSignature>,
) -> 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<Item = &'a AggNonce>,
partial_signature_groups: impl IntoIterator<Item = S>,
) -> Result<Vec<AdaptorSignature>, 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 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()
}

105
src/contract/parameters.rs Normal file
View File

@@ -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<Player, u64>;
#[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<Player>,
/// 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.
pub expiry_payout: Option<PayoutWeights>,
/// 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<Amount, Error> {
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<BTreeSet<WinCondition>> {
// 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::<WinCondition>::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)
}
}

254
src/contract/split.rs Normal file
View File

@@ -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<Transaction>,
split_spend_infos: BTreeMap<WinCondition, SplitSpendInfo>,
}
/// 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<SplitTransactionBuildOutput, Error> {
let outcome_txs = &outcome_build_output.outcome_txs;
let mut split_spend_infos = BTreeMap::<WinCondition, SplitSpendInfo>::new();
let mut split_txs = Vec::<Transaction>::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,
&params.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<Scalar>,
secnonces: impl IntoIterator<Item = SecNonce>,
aggnonces: impl IntoIterator<Item = &'a AggNonce>,
) -> Result<BTreeMap<WinCondition, PartialSignature>, Error> {
let seckey = seckey.into();
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);
}
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<WinCondition, PubNonce>,
aggnonces: &BTreeMap<WinCondition, AggNonce>,
partial_signatures: &BTreeMap<WinCondition, PartialSignature>,
) -> 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<WinCondition, AggNonce>,
partial_signatures_by_win_cond: &'s BTreeMap<WinCondition, S>,
) -> Result<BTreeMap<WinCondition, CompactSignature>, Error>
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 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()
}

View File

@@ -22,6 +22,12 @@ impl From<musig2::errors::TweakError> for Error {
}
}
impl From<musig2::errors::VerifyError> for Error {
fn from(_: musig2::errors::VerifyError) -> Self {
Error
}
}
impl From<musig2::errors::SigningError> for Error {
fn from(_: musig2::errors::SigningError) -> Self {
Error

29
src/hashlock.rs Normal file
View File

@@ -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<R: rand::RngCore + rand::CryptoRng>(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<Preimage, hex::FromHexError> {
let mut preimage = [0u8; PREIMAGE_SIZE];
hex::decode_to_slice(s, &mut preimage)?;
Ok(preimage)
}

1501
src/lib.rs

File diff suppressed because it is too large Load Diff

34
src/parties.rs Normal file
View File

@@ -0,0 +1,34 @@
use secp::Point;
/// 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,
}
/// 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],
}

70
src/spend_info/funding.rs Normal file
View File

@@ -0,0 +1,70 @@
use bitcoin::{
sighash::{Prevouts, SighashCache},
Amount, ScriptBuf, TapSighash, TapSighashType, Transaction, TxOut,
};
use musig2::KeyAggContext;
use secp::Point;
use crate::{
errors::Error,
parties::{MarketMaker, Player},
};
#[derive(Debug, Clone)]
pub(crate) struct FundingSpendInfo {
key_agg_ctx: KeyAggContext,
funding_value: Amount,
}
impl FundingSpendInfo {
pub(crate) fn new<'p>(
market_maker: &MarketMaker,
players: impl IntoIterator<Item = &'p Player>,
funding_value: Amount,
) -> Result<FundingSpendInfo, Error> {
let mut pubkeys: Vec<Point> = 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(crate) fn key_agg_ctx(&self) -> &KeyAggContext {
&self.key_agg_ctx
}
/// Returns the TX locking script for funding the ticketed DLC multisig.
pub(crate) 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(crate) fn sighash_tx_outcome(
&self,
outcome_tx: &Transaction,
) -> Result<TapSighash, bitcoin::sighash::Error> {
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,
)
}
}

7
src/spend_info/mod.rs Normal file
View File

@@ -0,0 +1,7 @@
mod funding;
mod outcome;
mod split;
pub(crate) use funding::FundingSpendInfo;
pub(crate) use outcome::OutcomeSpendInfo;
pub(crate) use split::SplitSpendInfo;

241
src/spend_info/outcome.rs Normal file
View File

@@ -0,0 +1,241 @@
use bitcoin::{
key::constants::SCHNORR_SIGNATURE_SIZE,
opcodes::all::*,
sighash::{Prevouts, SighashCache},
taproot::{
LeafVersion, TaprootSpendInfo, TAPROOT_CONTROL_BASE_SIZE, TAPROOT_CONTROL_NODE_SIZE,
},
transaction::InputWeightPrediction,
Amount, ScriptBuf, TapLeafHash, TapSighash, TapSighashType, Transaction, TxOut,
};
use musig2::KeyAggContext;
use secp::Point;
use crate::{
errors::Error,
hashlock::PREIMAGE_SIZE,
parties::{MarketMaker, Player},
};
use std::collections::BTreeMap;
/// 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(crate) struct OutcomeSpendInfo {
untweaked_ctx: KeyAggContext,
tweaked_ctx: KeyAggContext,
outcome_value: Amount,
spend_info: TaprootSpendInfo,
winner_split_scripts: BTreeMap<Player, ScriptBuf>,
reclaim_script: ScriptBuf,
}
impl OutcomeSpendInfo {
pub(crate) fn new<W: IntoIterator<Item = Player>>(
winners: W,
market_maker: &MarketMaker,
outcome_value: Amount,
block_delta: u16,
) -> Result<Self, Error> {
let winners: Vec<Player> = winners.into_iter().collect();
let mut pubkeys: Vec<Point> = [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<Player, ScriptBuf> = winners
.iter()
.map(|&winner| {
// The winner split script, used by winning players to spend
// the outcome transaction using the split transaction.
//
// Input: <joint_sig> <preimage>
let script = bitcoin::script::Builder::new()
// Check ticket preimage: OP_SHA256 <ticket_hash> OP_EQUALVERIFY
.push_opcode(OP_SHA256)
.push_slice(winner.ticket_hash)
.push_opcode(OP_EQUALVERIFY)
// Check joint signature: <joint_pk> 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: <mm_pubkey> 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)
}
pub(crate) fn key_agg_ctx_untweaked(&self) -> &KeyAggContext {
&self.untweaked_ctx
}
pub(crate) fn key_agg_ctx_tweaked(&self) -> &KeyAggContext {
&self.tweaked_ctx
}
/// Returns the TX locking script for this this outcome contract.
pub(crate) fn script_pubkey(&self) -> ScriptBuf {
ScriptBuf::new_p2tr_tweaked(self.spend_info.output_key())
}
pub(crate) fn outcome_value(&self) -> Amount {
self.outcome_value
}
/// 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(crate) 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:
// <joint_sig> <preimage> <script> <ctrl_block>
InputWeightPrediction::new(
0,
[
SCHNORR_SIGNATURE_SIZE, // BIP340 schnorr signature
PREIMAGE_SIZE, // Ticket preimage
outcome_script_len, // Script
TAPROOT_CONTROL_BASE_SIZE + TAPROOT_CONTROL_NODE_SIZE * max_taptree_depth, // Control block
],
)
}
/// Computes the input weight when spending the output of the outcome TX
/// as an input of the reclaim TX. This assumes the market maker's reclaim
/// tapscript leaf is being used to build a witness.
pub(crate) fn input_weight_for_reclaim_tx(&self) -> InputWeightPrediction {
let reclaim_control_block = self
.spend_info
.control_block(&(self.reclaim_script.clone(), LeafVersion::TapScript))
.expect("reclaim script cannot be missing");
// The witness stack for the reclaim TX which spends the outcome TX is:
// <market_maker_sig> <script> <ctrl_block>
InputWeightPrediction::new(
0,
[
SCHNORR_SIGNATURE_SIZE, // BIP340 schnorr signature
self.reclaim_script.len(), // Script
reclaim_control_block.size(), // Control block
],
)
}
/// Compute the signature hash for a given split transaction.
pub(crate) fn sighash_tx_split(
&self,
split_tx: &Transaction,
winner: &Player,
) -> Result<TapSighash, Error> {
let outcome_prevouts = [TxOut {
script_pubkey: self.script_pubkey(),
value: self.outcome_value,
}];
let split_script = self.winner_split_scripts.get(winner).ok_or(Error)?;
let leaf_hash = TapLeafHash::from_script(split_script, LeafVersion::TapScript);
let sighash = SighashCache::new(split_tx).taproot_script_spend_signature_hash(
0,
&Prevouts::All(&outcome_prevouts),
leaf_hash,
TapSighashType::Default,
)?;
Ok(sighash)
}
/// Compute the signature hash for a given split transaction.
pub(crate) fn sighash_tx_reclaim(&self, split_tx: &Transaction) -> Result<TapSighash, Error> {
let outcome_prevouts = [TxOut {
script_pubkey: self.script_pubkey(),
value: self.outcome_value,
}];
let leaf_hash = TapLeafHash::from_script(&self.reclaim_script, LeafVersion::TapScript);
let sighash = SighashCache::new(split_tx).taproot_script_spend_signature_hash(
0,
&Prevouts::All(&outcome_prevouts),
leaf_hash,
TapSighashType::Default,
)?;
Ok(sighash)
}
}

212
src/spend_info/split.rs Normal file
View File

@@ -0,0 +1,212 @@
use bitcoin::{
key::constants::SCHNORR_SIGNATURE_SIZE,
opcodes::all::*,
taproot::{LeafVersion, TaprootSpendInfo},
transaction::InputWeightPrediction,
Amount, ScriptBuf,
};
use musig2::KeyAggContext;
use secp::Point;
use crate::{
errors::Error,
hashlock::PREIMAGE_SIZE,
parties::{MarketMaker, Player},
};
/// Represents a taproot contract for a specific player's split TX payout output.
/// This tree has three nodes:
///
/// 1. A relative-timelocked hash-lock which pays to the player if they know their ticket
/// preimage after one round of block delay.
///
/// 2. A relative-timelock which pays to the market maker after two rounds of block delay.
///
/// 3. A hash-lock which pays to the market maker immediately if they learn the
// payout preimage from the player.
#[derive(Debug, Clone)]
pub(crate) struct SplitSpendInfo {
untweaked_ctx: KeyAggContext,
tweaked_ctx: KeyAggContext,
payout_value: Amount,
spend_info: TaprootSpendInfo,
winner: Player,
win_script: ScriptBuf,
reclaim_script: ScriptBuf,
sellback_script: ScriptBuf,
}
impl SplitSpendInfo {
pub(crate) fn new(
winner: Player,
market_maker: &MarketMaker,
payout_value: Amount,
block_delta: u16,
) -> Result<SplitSpendInfo, Error> {
let mut pubkeys = vec![market_maker.pubkey, winner.pubkey];
pubkeys.sort();
let untweaked_ctx = KeyAggContext::new(pubkeys)?;
let joint_payout_pubkey: Point = untweaked_ctx.aggregated_pubkey();
// The win script, used by a ticketholding winner to claim their
// payout on-chain if the market maker doesn't cooperate.
//
// Inputs: <player_sig> <preimage>
let win_script = bitcoin::script::Builder::new()
// Check relative locktime: <delta> OP_CSV OP_DROP
.push_int(block_delta as i64)
.push_opcode(OP_CSV)
.push_opcode(OP_DROP)
// Check ticket preimage: OP_SHA256 <ticket_hash> OP_EQUALVERIFY
.push_opcode(OP_SHA256)
.push_slice(winner.ticket_hash)
.push_opcode(OP_EQUALVERIFY)
// Check signature: <winner_pk> OP_CHECKSIG
.push_slice(winner.pubkey.serialize_xonly())
.push_opcode(OP_CHECKSIG)
.into_script();
// The reclaim script, used by the market maker to reclaim their capital
// if the player never paid for their ticket preimage.
//
// Inputs: <mm_sig>
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: <mm_pubkey> OP_CHECKSIG
.push_slice(market_maker.pubkey.serialize_xonly())
.push_opcode(OP_CHECKSIG)
.into_script();
// The sellback script, used by the market maker to reclaim their capital
// if the player agrees to sell their payout output from the split TX back
// to the market maker.
//
// Inputs: <mm_sig> <payout_preimage>
let sellback_script = bitcoin::script::Builder::new()
// Check payout preimage: OP_SHA256 <payout_hash> OP_EQUALVERIFY
.push_opcode(OP_SHA256)
.push_slice(winner.payout_hash)
.push_opcode(OP_EQUALVERIFY)
// Check signature: <mm_pubkey> OP_CHECKSIG
.push_slice(market_maker.pubkey.serialize_xonly())
.push_opcode(OP_CHECKSIG)
.into_script();
let weighted_script_leaves = [
(2, sellback_script.clone()),
(1, win_script.clone()),
(1, reclaim_script.clone()),
];
let tr_spend_info = TaprootSpendInfo::with_huffman_tree(
secp256k1::SECP256K1,
joint_payout_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 split_spend_info = SplitSpendInfo {
untweaked_ctx,
tweaked_ctx,
payout_value,
spend_info: tr_spend_info,
winner,
win_script,
reclaim_script,
sellback_script,
};
Ok(split_spend_info)
}
pub(crate) fn key_agg_ctx_untweaked(&self) -> &KeyAggContext {
&self.untweaked_ctx
}
pub(crate) fn key_agg_ctx_tweaked(&self) -> &KeyAggContext {
&self.tweaked_ctx
}
/// Returns the TX locking script for this player's split TX output contract.
pub(crate) fn script_pubkey(&self) -> ScriptBuf {
ScriptBuf::new_p2tr_tweaked(self.spend_info.output_key())
}
pub(crate) fn payout_value(&self) -> Amount {
self.payout_value
}
/// Computes the input weight when spending an output of the split TX
/// as an input of the player's win TX. This assumes the player's win script
/// leaf is being used to unlock the taproot tree.
pub(crate) fn input_weight_for_win_tx(&self) -> InputWeightPrediction {
let win_control_block = self
.spend_info
.control_block(&(self.win_script.clone(), LeafVersion::TapScript))
.expect("win script cannot be missing");
// The witness stack for the win TX which spends a split TX output is:
// <player_sig> <preimage> <script> <ctrl_block>
InputWeightPrediction::new(
0,
[
SCHNORR_SIGNATURE_SIZE, // BIP340 schnorr signature
PREIMAGE_SIZE, // Ticket preimage
self.win_script.len(), // Script
win_control_block.size(), // Control block
],
)
}
/// Computes the input weight when spending an output of the split TX
/// as an input of the market maker's reclaim TX. This assumes the market
/// maker's reclaim script leaf is being used to unlock the taproot tree.
pub(crate) fn input_weight_for_reclaim_tx(&self) -> InputWeightPrediction {
let reclaim_control_block = self
.spend_info
.control_block(&(self.reclaim_script.clone(), LeafVersion::TapScript))
.expect("reclaim script cannot be missing");
// The witness stack for the reclaim TX which spends a split TX output is:
// <player_sig> <script> <ctrl_block>
InputWeightPrediction::new(
0,
[
SCHNORR_SIGNATURE_SIZE, // BIP340 schnorr signature
self.reclaim_script.len(), // Script
reclaim_control_block.size(), // Control block
],
)
}
/// Computes the input weight when spending an output of the split TX
/// as an input of the sellback TX. This assumes the market maker's sellback
/// script leaf is being used to unlock the taproot tree.
pub(crate) fn input_weight_for_sellback_tx(&self) -> InputWeightPrediction {
let sellback_control_block = self
.spend_info
.control_block(&(self.sellback_script.clone(), LeafVersion::TapScript))
.expect("sellback script cannot be missing");
// The witness stack for the sellback TX which spends a split TX output is:
// <mm_sig> <payout_preimage> <script> <ctrl_block>
InputWeightPrediction::new(
0,
[
SCHNORR_SIGNATURE_SIZE, // BIP340 schnorr signature
PREIMAGE_SIZE, // Payout preimage
self.sellback_script.len(), // Script
sellback_control_block.size(), // Control block
],
)
}
// pub(crate) fn sighash_tx_win(&self)
}