From 595c930b853d1633a7bbfc0a0e9bf81ff47493d2 Mon Sep 17 00:00:00 2001 From: conduition Date: Sat, 2 Mar 2024 02:31:47 +0000 Subject: [PATCH] initial work on high-level state machine for musig signing This is my first go at a higher level API which signers will use to construct and sign the numerous transactions needed for executing a Ticketed DLC. It revolves around the SigningSession struct type, which has an API constrainted by its current state. --- Cargo.lock | 3 + Cargo.toml | 2 +- src/contract/mod.rs | 117 +++++++++++++++++++++ src/contract/outcome.rs | 16 +-- src/contract/split.rs | 19 +--- src/lib.rs | 218 +++++++++++++++++++++++++++++++++++++++- 6 files changed, 349 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 269343e..0d92bc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -163,6 +163,7 @@ checksum = "1f43e7abc6e724a2c8caf0e558ab838d71e70826f98df85da6a607243efbc83f" dependencies = [ "base16ct", "once_cell", + "rand", "secp", "secp256k1", "sha2", @@ -219,6 +220,7 @@ checksum = "787bed714ffe1439d1f8ce84e2ceed8daf6d5737f5422108a5de88030fc597b6" dependencies = [ "base16ct", "once_cell", + "rand", "secp256k1", "subtle", ] @@ -230,6 +232,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d24b59d129cdadea20aea4fb2352fa053712e5d713eee47d700cd4b2bc002f10" dependencies = [ "bitcoin_hashes", + "rand", "secp256k1-sys", ] diff --git a/Cargo.toml b/Cargo.toml index dbbc4d1..dbe4608 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ keywords = ["dlc", "smart", "contract", "ticket", "auction"] [dependencies] bitcoin = { version = "0.31.1" } hex = "0.4.3" -musig2 = { version = "0.0.4" } +musig2 = { version = "0.0.4", features = ["rand"] } rand = "0.8.5" secp = { version = "0.2.1" } secp256k1 = { version = "0.28.2", features = ["global-context"] } diff --git a/src/contract/mod.rs b/src/contract/mod.rs index 7380f12..472dde5 100644 --- a/src/contract/mod.rs +++ b/src/contract/mod.rs @@ -163,4 +163,121 @@ impl ContractParameters { Some(win_conditions_to_sign) } + + pub fn sigmap_for_pubkey(&self, pubkey: Point) -> Option> { + 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) + } + + /// 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<()> { + let mut all_win_conditions = BTreeMap::new(); + for (&outcome, payout_map) in self.outcome_payouts.iter() { + all_win_conditions.extend( + payout_map + .keys() + .map(|&winner| (WinCondition { winner, outcome }, ())), + ); + } + + SigMap { + by_outcome: self + .outcome_payouts + .iter() + .map(|(&outcome, _)| (outcome, ())) + .collect(), + by_win_condition: all_win_conditions, + } + } +} + +/// 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 { + pub by_outcome: BTreeMap, + pub by_win_condition: BTreeMap, +} + +impl SigMap { + pub fn map(self, map_outcomes: F1, map_win_conditions: F2) -> SigMap + 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(self, mut map_fn: F) -> SigMap + 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(&self, other: &SigMap) -> 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 + } } diff --git a/src/contract/outcome.rs b/src/contract/outcome.rs index 1415a80..397d0df 100644 --- a/src/contract/outcome.rs +++ b/src/contract/outcome.rs @@ -32,6 +32,11 @@ impl OutcomeTransactionBuildOutput { &self.outcome_spend_infos } + /// Return the funding transaction's spending info object. + pub(crate) fn funding_spend_info(&self) -> &FundingSpendInfo { + &self.funding_spend_info + } + /// Returns the number of [`musig2`] partial signatures required by each player /// in the DLC (and the market maker). /// @@ -113,8 +118,8 @@ pub(crate) fn partial_sign_outcome_txs<'a>( params: &ContractParameters, outcome_build_out: &OutcomeTransactionBuildOutput, seckey: Scalar, - secnonces: impl IntoIterator, - aggnonces: impl IntoIterator, + mut secnonces: BTreeMap, + aggnonces: &BTreeMap, ) -> Result, Error> { let outcome_txs = &outcome_build_out.outcome_txs; let funding_spend_info = &outcome_build_out.funding_spend_info; @@ -127,12 +132,9 @@ pub(crate) fn partial_sign_outcome_txs<'a>( let mut outcome_partial_sigs = BTreeMap::::new(); - let mut aggnonce_iter = aggnonces.into_iter(); - let mut secnonce_iter = secnonces.into_iter(); - for (&outcome, outcome_tx) in outcome_txs { - let aggnonce = aggnonce_iter.next().ok_or(Error)?; // must provide enough aggnonces - let secnonce = secnonce_iter.next().ok_or(Error)?; // must provide enough secnonces + let aggnonce = aggnonces.get(&outcome).ok_or(Error)?; // must provide all aggnonces + let secnonce = secnonces.remove(&outcome).ok_or(Error)?; // must provide all secnonces // Hash the outcome TX. let sighash = funding_spend_info.sighash_tx_outcome(outcome_tx)?; diff --git a/src/contract/split.rs b/src/contract/split.rs index a3cbe35..bb3509b 100644 --- a/src/contract/split.rs +++ b/src/contract/split.rs @@ -27,14 +27,6 @@ impl SplitTransactionBuildOutput { pub(crate) fn split_txs(&self) -> &BTreeMap { &self.split_txs } - - /// Returns the number of [`musig2`] partial signatures required by each player - /// in the DLC (and the market maker). This is the sum of all possible win conditions - /// across every split transaction (i.e. counting the number of winners in every - /// possible outcome). - pub(crate) fn signatures_required(&self) -> usize { - self.split_spend_infos.len() - } } /// Build the set of split transactions which splits an outcome TX into per-player @@ -123,8 +115,8 @@ pub(crate) fn partial_sign_split_txs<'a>( outcome_build_out: &OutcomeTransactionBuildOutput, split_build_out: &SplitTransactionBuildOutput, seckey: Scalar, - secnonces: impl IntoIterator, - aggnonces: impl IntoIterator, + mut secnonces: BTreeMap, + aggnonces: &BTreeMap, ) -> Result, Error> { let pubkey = seckey.base_point_mul(); @@ -137,17 +129,14 @@ pub(crate) fn partial_sign_split_txs<'a>( return Ok(partial_signatures); } - 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_build_out .split_txs() .get(&win_cond.outcome) .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 + let aggnonce = aggnonces.get(&win_cond).ok_or(Error)?; // must provide all aggnonces + let secnonce = secnonces.remove(&win_cond).ok_or(Error)?; // must provide all secnonces let outcome_spend_info = outcome_build_out .outcome_spend_infos() diff --git a/src/lib.rs b/src/lib.rs index a435dc1..21ff2fc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,10 +1,222 @@ //! THIS IS A PLACEHOLDER PACKAGE. DO NOT INSTALL THIS. pub(crate) mod consts; +pub(crate) mod errors; +pub(crate) mod oracles; +pub(crate) mod parties; pub(crate) mod spend_info; pub mod contract; -pub mod errors; pub mod hashlock; -pub mod oracles; -pub mod parties; + +pub use secp; + +pub use parties::{MarketMaker, Player}; + +use contract::outcome::{ + build_outcome_txs, partial_sign_outcome_txs, OutcomeTransactionBuildOutput, +}; +use contract::split::{build_split_txs, partial_sign_split_txs, SplitTransactionBuildOutput}; +use contract::{ContractParameters, SigMap}; +use errors::Error; + +use bitcoin::{OutPoint, TxOut}; +use musig2::{AggNonce, PartialSignature, PubNonce, SecNonce}; +use secp::{Point, Scalar}; + +use std::collections::BTreeMap; + +/// Represents the combined output of building all transactions and precomputing +/// all necessary data for a ticketed DLC. +pub struct TicketedDLC { + params: ContractParameters, + outcome_tx_build: OutcomeTransactionBuildOutput, + split_tx_build: SplitTransactionBuildOutput, +} + +impl TicketedDLC { + /// Construct all ticketed DLC transactions and cache precomputed data for later signing. + pub fn new( + params: ContractParameters, + funding_outpoint: OutPoint, + ) -> Result { + let outcome_tx_build = build_outcome_txs(¶ms, funding_outpoint)?; + let split_tx_build = build_split_txs(¶ms, &outcome_tx_build)?; + + let txs = TicketedDLC { + params, + outcome_tx_build, + split_tx_build, + }; + Ok(txs) + } + + /// Return the expected transaction output which the market maker should include + /// in the funding transaction in order to fund the contract. + /// + /// This uses cached data which was computed during the initial contract construction, + /// and so is more efficient than [`ContractParameters::funding_output`]. + pub fn funding_output(&self) -> TxOut { + TxOut { + script_pubkey: self.outcome_tx_build.funding_spend_info().script_pubkey(), + value: self.params.funding_value, + } + } +} + +/// A marker trait used to constrain the API of [`SigningSession`]. +pub trait SigningSessionState {} + +/// A [`SigningSessionState`] state for the initial nonce-sharing +/// round of communication. +pub struct NonceSharingRound { + signing_key: Scalar, + our_secret_nonces: SigMap, + our_public_nonces: SigMap, +} + +/// A [`SigningSessionState`] state for the second signature-sharing +/// round of communication. This assumes a mesh topology between +/// signers, where every signer sends their partial signatures to +/// everyone else. +pub struct PartialSignatureSharingRound { + received_nonces: BTreeMap>, + aggregated_nonces: SigMap, + our_partial_signatures: SigMap, +} + +impl SigningSessionState for NonceSharingRound {} +impl SigningSessionState for PartialSignatureSharingRound {} + +/// This is a state machine to manage signing the various transactions in a [`TicketedDLC`]. +pub struct SigningSession { + dlc: TicketedDLC, + state: S, +} + +impl SigningSession { + /// Return a reference to the [`TicketedDLC`] inside this signing session. + pub fn dlc(&self) -> &TicketedDLC { + &self.dlc + } +} + +impl SigningSession { + pub fn new( + dlc: TicketedDLC, + mut rng: &mut R, + signing_key: impl Into, + ) -> Result, Error> { + let signing_key = signing_key.into(); + + let base_sigmap = dlc + .params + .sigmap_for_pubkey(signing_key.base_point_mul()) + .ok_or(Error)?; + + let our_secret_nonces = base_sigmap.map_values(|_| { + SecNonce::build(&mut rng) + .with_seckey(signing_key) + // .with_extra_info(&self.dlc.params.serialize()) // TODO + .build() + }); + + let our_public_nonces = our_secret_nonces + .by_ref() + .map_values(|secnonce| secnonce.public_nonce()); + + let session = SigningSession { + dlc, + state: NonceSharingRound { + signing_key: signing_key.into(), + our_secret_nonces, + our_public_nonces, + }, + }; + Ok(session) + } + + /// The public nonces we should send to other signers. + pub fn our_public_nonces(&self) -> &SigMap { + &self.state.our_public_nonces + } + + /// Receive the nonces from all other signers. + pub fn compute_all_signatures( + self, + mut received_nonces: BTreeMap>, + ) -> Result, Error> { + // Insert our own public nonces so that callers don't need + // to inject them manually. + received_nonces.insert( + self.state.signing_key.base_point_mul(), + self.state.our_public_nonces, + ); + + // Must receive nonces from all players and the market maker. + if !received_nonces.contains_key(&self.dlc.params.market_maker.pubkey) { + return Err(Error); + } + for player in self.dlc.params.players.iter() { + if !received_nonces.contains_key(&player.pubkey) { + return Err(Error); + } + } + + // The expected sigmaps each signer must provide nonces for. + let base_sigmaps: BTreeMap> = received_nonces + .keys() + .map(|&key| Ok((key, self.dlc.params.sigmap_for_pubkey(key).ok_or(Error)?))) + .collect::>()?; + + for (&signer_pubkey, nonces) in received_nonces.iter() { + // All signers' sigmaps must match exactly. + if !nonces.is_mirror(&base_sigmaps[&signer_pubkey]) { + return Err(Error); + } + } + + let aggregated_nonces: SigMap = self.dlc.params.full_sigmap().map( + |outcome, _| { + received_nonces + .values() + .filter_map(|nonce_sigmap| nonce_sigmap.by_outcome.get(&outcome)) + .sum::() + }, + |win_cond, _| { + received_nonces + .values() + .filter_map(|nonce_sigmap| nonce_sigmap.by_win_condition.get(&win_cond)) + .sum::() + }, + ); + + let our_partial_signatures = SigMap { + by_outcome: partial_sign_outcome_txs( + &self.dlc.params, + &self.dlc.outcome_tx_build, + self.state.signing_key, + self.state.our_secret_nonces.by_outcome, + &aggregated_nonces.by_outcome, + )?, + by_win_condition: partial_sign_split_txs( + &self.dlc.params, + &self.dlc.outcome_tx_build, + &self.dlc.split_tx_build, + self.state.signing_key, + self.state.our_secret_nonces.by_win_condition, + &aggregated_nonces.by_win_condition, + )?, + }; + + let session = SigningSession { + dlc: self.dlc, + state: PartialSignatureSharingRound { + received_nonces, + aggregated_nonces, + our_partial_signatures, + }, + }; + Ok(session) + } +}