mirror of
https://github.com/conduition/dlctix.git
synced 2026-01-30 05:05:06 +01:00
improve memory overhead by referring to players by index
We were using btrees indexed by players, which required each btree to store the player in memory. This was inefficient. Instead, we now index by usize, which is a player index in the sorted set of players in ContractParameters.
This commit is contained in:
@@ -43,18 +43,18 @@ pub(crate) fn fee_subtract_safe(
|
||||
///
|
||||
/// Returns an error if any output value is negative, or is less than the
|
||||
/// given dust threshold.
|
||||
pub(crate) fn fee_calc_shared<'k, I, O, T>(
|
||||
pub(crate) fn fee_calc_shared<I, O, T>(
|
||||
available_coins: Amount,
|
||||
fee_rate: FeeRate,
|
||||
input_weights: I,
|
||||
output_spk_lens: O,
|
||||
dust_threshold: Amount,
|
||||
payout_map: &'k BTreeMap<T, u64>,
|
||||
) -> Result<BTreeMap<&'k T, Amount>, Error>
|
||||
payout_map: &BTreeMap<T, u64>,
|
||||
) -> Result<BTreeMap<T, Amount>, Error>
|
||||
where
|
||||
I: IntoIterator<Item = InputWeightPrediction>,
|
||||
O: IntoIterator<Item = usize>,
|
||||
T: Clone + Ord,
|
||||
T: Copy + Ord,
|
||||
{
|
||||
let fee_total = fee_calc_safe(fee_rate, input_weights, output_spk_lens)?;
|
||||
|
||||
@@ -65,7 +65,7 @@ where
|
||||
// Payout amounts are computed by using relative weights.
|
||||
payout_map
|
||||
.iter()
|
||||
.map(|(key, &weight)| {
|
||||
.map(|(&key, &weight)| {
|
||||
let payout = available_coins * weight / total_weight;
|
||||
let payout_value = fee_subtract_safe(payout, fee_shared, dust_threshold)?;
|
||||
Ok((key, payout_value))
|
||||
|
||||
@@ -15,6 +15,10 @@ use crate::{
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
/// A type alias for clarity. Players in the DLC are often referred to by their
|
||||
/// index in the sorted set of players.
|
||||
pub type PlayerIndex = usize;
|
||||
|
||||
/// 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
|
||||
@@ -26,7 +30,7 @@ use std::collections::{BTreeMap, BTreeSet};
|
||||
///
|
||||
/// 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>;
|
||||
pub type PayoutWeights = BTreeMap<PlayerIndex, u64>;
|
||||
|
||||
/// Represents the parameters which all players and the market maker must agree on
|
||||
/// to construct a ticketed DLC.
|
||||
@@ -89,7 +93,7 @@ pub enum Outcome {
|
||||
#[derive(Clone, Copy, Debug, Ord, PartialOrd, Eq, PartialEq, Hash)]
|
||||
pub struct WinCondition {
|
||||
pub outcome: Outcome,
|
||||
pub winner: Player,
|
||||
pub player_index: PlayerIndex,
|
||||
}
|
||||
|
||||
impl ContractParameters {
|
||||
@@ -120,14 +124,14 @@ impl ContractParameters {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
for (player, &weight) in payout_map.iter() {
|
||||
for (&player_index, &weight) in payout_map.iter() {
|
||||
// Check for zero payout weights.
|
||||
if weight == 0 {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
// Check for unregistered players.
|
||||
if !self.players.contains(player) {
|
||||
// Check for out-of-bounds player indexes.
|
||||
if player_index >= self.players.len() {
|
||||
return Err(Error);
|
||||
}
|
||||
}
|
||||
@@ -151,6 +155,12 @@ impl ContractParameters {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Return a sorted vector of players. Each player's index in this vector
|
||||
/// should be used as an identifier for the DLC.
|
||||
pub fn sorted_players<'a>(&'a self) -> Vec<&'a Player> {
|
||||
self.players.iter().collect()
|
||||
}
|
||||
|
||||
/// Returns the transaction output which the funding transaction should pay to.
|
||||
///
|
||||
/// Avoid overusing this method, as it recomputes the aggregated key every time
|
||||
@@ -169,14 +179,21 @@ impl ContractParameters {
|
||||
Ok(outcome_value)
|
||||
}
|
||||
|
||||
/// Returns the set of players which this pubkey can sign for.
|
||||
/// Returns the set of player indexes 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> {
|
||||
pub fn players_controlled_by_pubkey(&self, pubkey: Point) -> BTreeSet<PlayerIndex> {
|
||||
self.players
|
||||
.iter()
|
||||
.filter(|player| player.pubkey == pubkey)
|
||||
.enumerate()
|
||||
.filter_map(|(i, player)| {
|
||||
if player.pubkey == pubkey {
|
||||
Some(i)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -211,12 +228,16 @@ impl ContractParameters {
|
||||
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 }),
|
||||
);
|
||||
win_conditions_to_sign.extend(payout_map.keys().filter_map(|player_index| {
|
||||
if is_market_maker || controlling_players.contains(player_index) {
|
||||
Some(WinCondition {
|
||||
player_index: *player_index,
|
||||
outcome,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
Some(win_conditions_to_sign)
|
||||
@@ -239,11 +260,10 @@ impl ContractParameters {
|
||||
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.extend(payout_map.keys().map(|&player_index| WinCondition {
|
||||
player_index,
|
||||
outcome,
|
||||
}));
|
||||
}
|
||||
all_win_conditions
|
||||
}
|
||||
|
||||
@@ -45,6 +45,8 @@ pub(crate) fn build_outcome_txs(
|
||||
params: &ContractParameters,
|
||||
funding_outpoint: OutPoint,
|
||||
) -> Result<OutcomeTransactionBuildOutput, Error> {
|
||||
let all_players = params.sorted_players();
|
||||
|
||||
let funding_input = TxIn {
|
||||
previous_output: funding_outpoint,
|
||||
sequence: Sequence::MAX,
|
||||
@@ -58,6 +60,7 @@ pub(crate) fn build_outcome_txs(
|
||||
.map(|(&outcome, payout_map)| {
|
||||
let winners = payout_map.keys().copied();
|
||||
let spend_info = OutcomeSpendInfo::new(
|
||||
&all_players,
|
||||
winners,
|
||||
¶ms.market_maker,
|
||||
outcome_value,
|
||||
|
||||
@@ -6,10 +6,9 @@ use secp::{Point, Scalar};
|
||||
|
||||
use crate::{
|
||||
consts::{P2TR_DUST_VALUE, P2TR_SCRIPT_PUBKEY_SIZE},
|
||||
contract::{self, fees, outcome::OutcomeTransactionBuildOutput},
|
||||
contract::{self, fees, outcome::OutcomeTransactionBuildOutput, PlayerIndex},
|
||||
contract::{ContractParameters, Outcome, WinCondition},
|
||||
errors::Error,
|
||||
parties::Player,
|
||||
spend_info::SplitSpendInfo,
|
||||
};
|
||||
|
||||
@@ -42,6 +41,8 @@ pub(crate) fn build_split_txs(
|
||||
params: &ContractParameters,
|
||||
outcome_build_output: &OutcomeTransactionBuildOutput,
|
||||
) -> Result<SplitTransactionBuildOutput, Error> {
|
||||
let players = params.sorted_players();
|
||||
|
||||
let mut split_spend_infos = BTreeMap::<WinCondition, SplitSpendInfo>::new();
|
||||
let mut split_txs = BTreeMap::<Outcome, Transaction>::new();
|
||||
|
||||
@@ -54,7 +55,7 @@ pub(crate) fn build_split_txs(
|
||||
// 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 payout_values: BTreeMap<&Player, Amount> = fees::fee_calc_shared(
|
||||
let payout_values: BTreeMap<PlayerIndex, Amount> = fees::fee_calc_shared(
|
||||
outcome_spend_info.outcome_value(),
|
||||
params.fee_rate,
|
||||
[input_weight],
|
||||
@@ -71,7 +72,8 @@ pub(crate) fn build_split_txs(
|
||||
|
||||
// payout_values is a btree, so outputs are automatically sorted by player.
|
||||
let mut split_tx_outputs = Vec::with_capacity(payout_map.len());
|
||||
for (&&player, &payout_value) in payout_values.iter() {
|
||||
for (player_index, payout_value) in payout_values {
|
||||
let &player = players.get(player_index).ok_or(Error)?;
|
||||
let split_spend_info = SplitSpendInfo::new(
|
||||
player,
|
||||
¶ms.market_maker,
|
||||
@@ -85,7 +87,7 @@ pub(crate) fn build_split_txs(
|
||||
});
|
||||
|
||||
let win_cond = WinCondition {
|
||||
winner: player,
|
||||
player_index,
|
||||
outcome,
|
||||
};
|
||||
split_spend_infos.insert(win_cond, split_spend_info);
|
||||
@@ -151,7 +153,7 @@ pub(crate) fn partial_sign_split_txs(
|
||||
.ok_or(Error)?;
|
||||
|
||||
// Hash the split TX.
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?;
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?;
|
||||
|
||||
// Partially sign the sighash.
|
||||
// We must use the untweaked musig key to sign the split script spend,
|
||||
@@ -205,7 +207,7 @@ pub(crate) fn verify_split_tx_partial_signatures(
|
||||
.ok_or(Error)?;
|
||||
|
||||
// Hash the split TX.
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?;
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?;
|
||||
|
||||
// Verifies the player's partial signature on the split TX for one specific script path spend.
|
||||
musig2::verify_partial(
|
||||
@@ -257,7 +259,7 @@ where
|
||||
.ok_or(Error)?;
|
||||
|
||||
// Hash the split TX.
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?;
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?;
|
||||
|
||||
let compact_sig = musig2::aggregate_partial_signatures(
|
||||
outcome_spend_info.key_agg_ctx_untweaked(),
|
||||
@@ -306,7 +308,7 @@ pub(crate) fn verify_split_tx_aggregated_signatures(
|
||||
.key_agg_ctx_untweaked()
|
||||
.aggregated_pubkey();
|
||||
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.winner)?;
|
||||
let sighash = outcome_spend_info.sighash_tx_split(split_tx, &win_cond.player_index)?;
|
||||
|
||||
let batch_row = BatchVerificationRow::from_signature(
|
||||
winners_joint_pubkey,
|
||||
@@ -340,7 +342,7 @@ pub(crate) fn split_tx_prevout<'x>(
|
||||
let payout_map = params.outcome_payouts.get(&win_cond.outcome).ok_or(Error)?;
|
||||
let split_tx_output_index = payout_map
|
||||
.keys()
|
||||
.position(|p| p == &win_cond.winner)
|
||||
.position(|&player_index| player_index == win_cond.player_index)
|
||||
.ok_or(Error)?;
|
||||
|
||||
let input = TxIn {
|
||||
|
||||
31
src/lib.rs
31
src/lib.rs
@@ -27,7 +27,7 @@ use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
};
|
||||
|
||||
pub use contract::{ContractParameters, Outcome, SigMap, WinCondition};
|
||||
pub use contract::{ContractParameters, Outcome, PlayerIndex, SigMap, WinCondition};
|
||||
pub use oracles::EventAnnouncement;
|
||||
pub use parties::{MarketMaker, Player};
|
||||
|
||||
@@ -561,9 +561,17 @@ impl SignedContract {
|
||||
win_cond: &WinCondition,
|
||||
ticket_preimage: Preimage,
|
||||
) -> Result<Transaction, Error> {
|
||||
let winner = self
|
||||
.dlc
|
||||
.params
|
||||
.sorted_players()
|
||||
.get(win_cond.player_index)
|
||||
.cloned()
|
||||
.ok_or(Error)?;
|
||||
|
||||
// Verify the preimage will unlock this specific player's split TX
|
||||
// condition.
|
||||
if sha256(&ticket_preimage) != win_cond.winner.ticket_hash {
|
||||
if sha256(&ticket_preimage) != winner.ticket_hash {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
@@ -580,8 +588,11 @@ impl SignedContract {
|
||||
.get(&win_cond.outcome)
|
||||
.ok_or(Error)?;
|
||||
|
||||
let witness =
|
||||
outcome_spend_info.witness_tx_split(signature, ticket_preimage, &win_cond.winner)?;
|
||||
let witness = outcome_spend_info.witness_tx_split(
|
||||
signature,
|
||||
ticket_preimage,
|
||||
&win_cond.player_index,
|
||||
)?;
|
||||
|
||||
let mut split_tx = self
|
||||
.unsigned_split_tx(&win_cond.outcome)
|
||||
@@ -693,10 +704,18 @@ impl SignedContract {
|
||||
ticket_preimage: Preimage,
|
||||
player_secret_key: impl Into<Scalar>,
|
||||
) -> Result<(), Error> {
|
||||
let winner = self
|
||||
.dlc
|
||||
.params
|
||||
.sorted_players()
|
||||
.get(win_cond.player_index)
|
||||
.cloned()
|
||||
.ok_or(Error)?;
|
||||
|
||||
let player_secret_key = player_secret_key.into();
|
||||
if player_secret_key.base_point_mul() != win_cond.winner.pubkey {
|
||||
if player_secret_key.base_point_mul() != winner.pubkey {
|
||||
return Err(Error);
|
||||
} else if sha256(&ticket_preimage) != win_cond.winner.ticket_hash {
|
||||
} else if sha256(&ticket_preimage) != winner.ticket_hash {
|
||||
return Err(Error);
|
||||
}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ pub struct MarketMaker {
|
||||
/// 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)]
|
||||
#[derive(Debug, Clone, Ord, PartialOrd, Hash, Eq, PartialEq)]
|
||||
pub struct Player {
|
||||
/// An ephemeral public key controlled by the player.
|
||||
///
|
||||
|
||||
@@ -12,6 +12,7 @@ use musig2::{CompactSignature, KeyAggContext};
|
||||
use secp::{Point, Scalar};
|
||||
|
||||
use crate::{
|
||||
contract::PlayerIndex,
|
||||
errors::Error,
|
||||
hashlock::{Preimage, PREIMAGE_SIZE},
|
||||
parties::{MarketMaker, Player},
|
||||
@@ -40,29 +41,37 @@ pub(crate) struct OutcomeSpendInfo {
|
||||
tweaked_ctx: KeyAggContext,
|
||||
outcome_value: Amount,
|
||||
spend_info: TaprootSpendInfo,
|
||||
winner_split_scripts: BTreeMap<Player, ScriptBuf>,
|
||||
winner_split_scripts: BTreeMap<PlayerIndex, ScriptBuf>,
|
||||
reclaim_script: ScriptBuf,
|
||||
}
|
||||
|
||||
impl OutcomeSpendInfo {
|
||||
pub(crate) fn new<W: IntoIterator<Item = Player>>(
|
||||
winners: W,
|
||||
pub(crate) fn new(
|
||||
all_players: &[impl Borrow<Player>],
|
||||
winner_indexes: impl IntoIterator<Item = PlayerIndex>,
|
||||
market_maker: &MarketMaker,
|
||||
outcome_value: Amount,
|
||||
block_delta: u16,
|
||||
) -> Result<Self, Error> {
|
||||
let winners: Vec<Player> = winners.into_iter().collect();
|
||||
let winners: BTreeMap<PlayerIndex, &Player> = winner_indexes
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
let player = all_players.get(i).ok_or(Error)?;
|
||||
Ok((i, player.borrow()))
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
|
||||
let mut pubkeys: Vec<Point> = [market_maker.pubkey]
|
||||
.into_iter()
|
||||
.chain(winners.iter().map(|w| w.pubkey))
|
||||
.chain(winners.values().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| {
|
||||
let winner_split_scripts: BTreeMap<PlayerIndex, ScriptBuf> = winners
|
||||
.into_iter()
|
||||
.map(|(player_index, winner)| {
|
||||
// The winner split script, used by winning players to spend
|
||||
// the outcome transaction using the split transaction.
|
||||
//
|
||||
@@ -79,7 +88,7 @@ impl OutcomeSpendInfo {
|
||||
// Sequence number is enforced by multisig key: split TX is pre-signed.
|
||||
.into_script();
|
||||
|
||||
(winner, script)
|
||||
(player_index, script)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -202,13 +211,13 @@ impl OutcomeSpendInfo {
|
||||
pub(crate) fn sighash_tx_split(
|
||||
&self,
|
||||
split_tx: &Transaction,
|
||||
winner: &Player,
|
||||
player_index: &PlayerIndex,
|
||||
) -> 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 split_script = self.winner_split_scripts.get(player_index).ok_or(Error)?;
|
||||
let leaf_hash = TapLeafHash::from_script(split_script, LeafVersion::TapScript);
|
||||
|
||||
let sighash = SighashCache::new(split_tx).taproot_script_spend_signature_hash(
|
||||
@@ -225,9 +234,13 @@ impl OutcomeSpendInfo {
|
||||
&self,
|
||||
signature: &CompactSignature,
|
||||
ticket_preimage: Preimage,
|
||||
winner: &Player,
|
||||
player_index: &PlayerIndex,
|
||||
) -> Result<Witness, Error> {
|
||||
let split_script = self.winner_split_scripts.get(winner).ok_or(Error)?.clone();
|
||||
let split_script = self
|
||||
.winner_split_scripts
|
||||
.get(player_index)
|
||||
.ok_or(Error)?
|
||||
.clone();
|
||||
let control_block = self
|
||||
.spend_info
|
||||
.control_block(&(split_script.clone(), LeafVersion::TapScript))
|
||||
|
||||
@@ -32,7 +32,6 @@ pub(crate) struct SplitSpendInfo {
|
||||
tweaked_ctx: KeyAggContext,
|
||||
payout_value: Amount,
|
||||
spend_info: TaprootSpendInfo,
|
||||
winner: Player,
|
||||
win_script: ScriptBuf,
|
||||
reclaim_script: ScriptBuf,
|
||||
sellback_script: ScriptBuf,
|
||||
@@ -40,7 +39,7 @@ pub(crate) struct SplitSpendInfo {
|
||||
|
||||
impl SplitSpendInfo {
|
||||
pub(crate) fn new(
|
||||
winner: Player,
|
||||
winner: &Player,
|
||||
market_maker: &MarketMaker,
|
||||
payout_value: Amount,
|
||||
block_delta: u16,
|
||||
@@ -120,7 +119,6 @@ impl SplitSpendInfo {
|
||||
tweaked_ctx,
|
||||
payout_value,
|
||||
spend_info: tr_spend_info,
|
||||
winner,
|
||||
win_script,
|
||||
reclaim_script,
|
||||
sellback_script,
|
||||
|
||||
Reference in New Issue
Block a user