mirror of
https://github.com/conduition/dlctix.git
synced 2026-01-30 05:05:06 +01:00
refactor into multifile layout using functions
This commit is contained in:
19
src/consts.rs
Normal file
19
src/consts.rs
Normal 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
32
src/contract/fees.rs
Normal 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
6
src/contract/mod.rs
Normal 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
211
src/contract/outcome.rs
Normal 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,
|
||||
¶ms.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(¶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<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
105
src/contract/parameters.rs
Normal 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
254
src/contract/split.rs
Normal 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,
|
||||
¶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<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()
|
||||
}
|
||||
@@ -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
29
src/hashlock.rs
Normal 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
1501
src/lib.rs
File diff suppressed because it is too large
Load Diff
34
src/parties.rs
Normal file
34
src/parties.rs
Normal 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
70
src/spend_info/funding.rs
Normal 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
7
src/spend_info/mod.rs
Normal 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
241
src/spend_info/outcome.rs
Normal 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
212
src/spend_info/split.rs
Normal 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)
|
||||
}
|
||||
Reference in New Issue
Block a user