mirror of
https://github.com/conduition/dlctix.git
synced 2026-01-30 05:05:06 +01:00
292 lines
10 KiB
Rust
292 lines
10 KiB
Rust
pub(crate) mod fees;
|
|
pub(crate) mod outcome;
|
|
pub(crate) mod split;
|
|
|
|
use bitcoin::{transaction::InputWeightPrediction, Amount, FeeRate, TxOut};
|
|
use secp::Point;
|
|
|
|
use crate::{
|
|
consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE},
|
|
errors::Error,
|
|
oracles::EventAnnouncement,
|
|
parties::{MarketMaker, Player},
|
|
spend_info::FundingSpendInfo,
|
|
};
|
|
|
|
use std::collections::{BTreeMap, BTreeSet};
|
|
|
|
/// Represents a mapping of player to payout weight for a given outcome.
|
|
///
|
|
/// A player's payout under an outcome is proportional to the size of their payout weight
|
|
/// relative to the sum of payout weights of all other winners for that outcome.
|
|
///
|
|
/// ```not_rust
|
|
/// total_payout = contract_value * weights[player] / sum(weights)
|
|
/// ```
|
|
///
|
|
/// Players who should not receive a payout from an outcome MUST NOT be given an entry
|
|
/// in a `PayoutWeights` map.
|
|
pub type PayoutWeights = BTreeMap<Player, u64>;
|
|
|
|
/// Represents the parameters which all players and the market maker must agree on
|
|
/// to construct a ticketed DLC.
|
|
///
|
|
/// If all players use the same [`ContractParameters`], they should be able to
|
|
/// construct identical sets of outcome and split transactions, and exchange musig2
|
|
/// signatures thereupon.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ContractParameters {
|
|
/// The market maker who provides capital for the DLC ticketing process.
|
|
pub market_maker: MarketMaker,
|
|
|
|
/// The set of players in the DLC. Two players in the same DLC _may_ share
|
|
/// the same public key, but MUST NOT share the same payout hash or ticket hash.
|
|
pub players: BTreeSet<Player>,
|
|
|
|
/// The event whose outcome determines the payouts.
|
|
pub event: EventAnnouncement,
|
|
|
|
/// A mapping of payout weights under different outcomes. Attestation indexes should
|
|
/// align with [`self.event.outcome_messages`][EventAnnouncement::outcome_messages].
|
|
///
|
|
/// If this map does not contain a key of [`Outcome::Expiry`], then there is no expiry
|
|
/// condition, and the money simply remains locked in the funding outpoint until the
|
|
/// Oracle's attestation is found.
|
|
pub outcome_payouts: BTreeMap<Outcome, PayoutWeights>,
|
|
|
|
/// A default mining fee rate to be used for pre-signed transactions.
|
|
pub fee_rate: FeeRate,
|
|
|
|
/// The amount of on-chain capital which the market maker will provide when funding
|
|
/// the initial multisig deposit contract (after on-chain mining fees).
|
|
pub funding_value: Amount,
|
|
|
|
/// A reasonable number of blocks within which a transaction can confirm.
|
|
/// Used for enforcing relative locktime timeout spending conditions.
|
|
///
|
|
/// Reasonable values are:
|
|
///
|
|
/// - `72`: ~12 hours
|
|
/// - `144`: ~24 hours
|
|
/// - `432`: ~72 hours
|
|
/// - `1008`: ~1 week
|
|
pub relative_locktime_block_delta: u16,
|
|
}
|
|
|
|
/// Represents one possible outcome branch of the DLC. This includes both
|
|
/// outcomes attested-to by the Oracle, and expiry.
|
|
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
|
pub enum Outcome {
|
|
/// Indicates the oracle attested to a particular outcome of the given index.
|
|
Attestation(usize),
|
|
|
|
/// Indicates the oracle failed to attest to any outcome, and the event expiry
|
|
/// timelock was reached.
|
|
Expiry,
|
|
}
|
|
|
|
/// Points to a situation where a player wins a payout from the DLC.
|
|
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
|
pub struct WinCondition {
|
|
pub outcome: Outcome,
|
|
pub winner: Player,
|
|
}
|
|
|
|
impl ContractParameters {
|
|
/// Returns the transaction output which the funding transaction should pay to.
|
|
///
|
|
/// Avoid overusing this method, as it recomputes the aggregated key every time
|
|
/// it is invoked. Instead, prefer
|
|
/// [`TicketedDLC::funding_output`][crate::TicketedDLC::funding_output].
|
|
pub fn funding_output(&self) -> Result<TxOut, Error> {
|
|
let spend_info =
|
|
FundingSpendInfo::new(&self.market_maker, &self.players, self.funding_value)?;
|
|
Ok(spend_info.funding_output())
|
|
}
|
|
|
|
pub(crate) fn outcome_output_value(&self) -> Result<Amount, Error> {
|
|
let input_weights = [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)
|
|
}
|
|
|
|
/// Returns the set of players which this pubkey can sign for.
|
|
///
|
|
/// This might contain multiple players if the same key joined the DLC
|
|
/// with different ticket/payout hashes.
|
|
pub fn players_controlled_by_pubkey<'a>(&'a self, pubkey: Point) -> BTreeSet<&'a Player> {
|
|
self.players
|
|
.iter()
|
|
.filter(|player| player.pubkey == pubkey)
|
|
.collect()
|
|
}
|
|
|
|
/// Return the set of all win conditions which the given pubkey will need to sign
|
|
/// split transactions for.
|
|
///
|
|
/// If `pubkey` belongs to one or more players, this returns all [`WinCondition`]s
|
|
/// for outcomes in which the player or players are winners.
|
|
///
|
|
/// If `pubkey` belongs to the market maker, this returns every [`WinCondition`]
|
|
/// across the entire contract.
|
|
///
|
|
/// Returns `None` if the pubkey does not belong to any player in the DLC.
|
|
///
|
|
/// Returns an empty `BTreeSet` if the player is part of the DLC, but isn't due to
|
|
/// receive any payouts on any DLC outcome.
|
|
pub 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;
|
|
|
|
let controlling_players = self.players_controlled_by_pubkey(pubkey);
|
|
|
|
// Short circuit if this pubkey is not known.
|
|
if controlling_players.is_empty() && !is_market_maker {
|
|
return None;
|
|
}
|
|
|
|
let mut win_conditions_to_sign = BTreeSet::<WinCondition>::new();
|
|
for (&outcome, payout_map) in self.outcome_payouts.iter() {
|
|
// We want to sign the split TX for any win-conditions whose player is controlled
|
|
// by `pubkey`. If we're the market maker, we sign every win condition.
|
|
win_conditions_to_sign.extend(
|
|
payout_map
|
|
.keys()
|
|
.filter(|winner| is_market_maker || controlling_players.contains(winner))
|
|
.map(|&winner| WinCondition { winner, outcome }),
|
|
);
|
|
}
|
|
|
|
Some(win_conditions_to_sign)
|
|
}
|
|
|
|
pub fn sigmap_for_pubkey(&self, pubkey: Point) -> Option<SigMap<()>> {
|
|
let win_conditions = self.win_conditions_controlled_by_pubkey(pubkey)?;
|
|
let sigmap = SigMap {
|
|
by_outcome: self
|
|
.outcome_payouts
|
|
.iter()
|
|
.map(|(&outcome, _)| (outcome, ()))
|
|
.collect(),
|
|
by_win_condition: win_conditions.into_iter().map(|w| (w, ())).collect(),
|
|
};
|
|
Some(sigmap)
|
|
}
|
|
|
|
/// Return a full set of all possible win conditions for this DLC.
|
|
pub fn all_win_conditions(&self) -> BTreeSet<WinCondition> {
|
|
let mut all_win_conditions = BTreeSet::new();
|
|
for (&outcome, payout_map) in self.outcome_payouts.iter() {
|
|
all_win_conditions.extend(
|
|
payout_map
|
|
.keys()
|
|
.map(|&winner| WinCondition { winner, outcome }),
|
|
);
|
|
}
|
|
all_win_conditions
|
|
}
|
|
|
|
/// Returns an empty sigmap covering every outcome and every win condition.
|
|
/// This encompasses every possible message whose signatures are needed
|
|
/// to set up the contract.
|
|
pub fn full_sigmap(&self) -> SigMap<()> {
|
|
SigMap {
|
|
by_outcome: self
|
|
.outcome_payouts
|
|
.iter()
|
|
.map(|(&outcome, _)| (outcome, ()))
|
|
.collect(),
|
|
by_win_condition: self
|
|
.all_win_conditions()
|
|
.into_iter()
|
|
.map(|win_cond| (win_cond, ()))
|
|
.collect(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Represents a mapping of different signature requirements to some arbitrary type T.
|
|
/// This can be used to efficiently look up signatures, nonces, etc, for each
|
|
/// outcome transaction, and for different [`WinCondition`]s within each split transaction.
|
|
///
|
|
/// TODO serde serialization
|
|
#[derive(Debug, Clone, Eq, PartialEq, Default)]
|
|
pub struct SigMap<T> {
|
|
pub by_outcome: BTreeMap<Outcome, T>,
|
|
pub by_win_condition: BTreeMap<WinCondition, T>,
|
|
}
|
|
|
|
impl<T> SigMap<T> {
|
|
pub fn map<V, F1, F2>(self, map_outcomes: F1, map_win_conditions: F2) -> SigMap<V>
|
|
where
|
|
F1: Fn(Outcome, T) -> V,
|
|
F2: Fn(WinCondition, T) -> V,
|
|
{
|
|
SigMap {
|
|
by_outcome: self
|
|
.by_outcome
|
|
.into_iter()
|
|
.map(|(o, t)| (o, map_outcomes(o, t)))
|
|
.collect(),
|
|
by_win_condition: self
|
|
.by_win_condition
|
|
.into_iter()
|
|
.map(|(w, t)| (w, map_win_conditions(w, t)))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
pub fn map_values<V, F>(self, mut map_fn: F) -> SigMap<V>
|
|
where
|
|
F: FnMut(T) -> V,
|
|
{
|
|
SigMap {
|
|
by_outcome: self
|
|
.by_outcome
|
|
.into_iter()
|
|
.map(|(o, t)| (o, map_fn(t)))
|
|
.collect(),
|
|
by_win_condition: self
|
|
.by_win_condition
|
|
.into_iter()
|
|
.map(|(w, t)| (w, map_fn(t)))
|
|
.collect(),
|
|
}
|
|
}
|
|
|
|
pub fn by_ref(&self) -> SigMap<&T> {
|
|
SigMap {
|
|
by_outcome: self.by_outcome.iter().map(|(&k, v)| (k, v)).collect(),
|
|
by_win_condition: self.by_win_condition.iter().map(|(&k, v)| (k, v)).collect(),
|
|
}
|
|
}
|
|
|
|
/// Returns true if the given sigmap mirrors the keys of this sigmap exactly.
|
|
/// This means both sigmaps have entries for all the same outcomes and win
|
|
/// conditions, without any extra leftover entries.
|
|
pub fn is_mirror<V>(&self, other: &SigMap<V>) -> bool {
|
|
for outcome in self.by_outcome.keys() {
|
|
if !other.by_outcome.contains_key(outcome) {
|
|
return false;
|
|
}
|
|
}
|
|
for win_cond in self.by_win_condition.keys() {
|
|
if !other.by_win_condition.contains_key(win_cond) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if self.by_outcome.len() != other.by_outcome.len()
|
|
|| self.by_win_condition.len() != other.by_win_condition.len()
|
|
{
|
|
return false;
|
|
}
|
|
|
|
true
|
|
}
|
|
}
|