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:
conduition
2024-03-10 06:01:15 +00:00
parent 6d9bd1529e
commit 296cfaae66
8 changed files with 112 additions and 57 deletions

View File

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

View File

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

View File

@@ -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,
&params.market_maker,
outcome_value,

View File

@@ -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,
&params.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 {

View File

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

View File

@@ -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.
///

View File

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

View File

@@ -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,