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.
This commit is contained in:
conduition
2024-03-02 02:31:47 +00:00
parent 74bb611851
commit 595c930b85
6 changed files with 349 additions and 26 deletions

3
Cargo.lock generated
View File

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

View File

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

View File

@@ -163,4 +163,121 @@ impl ContractParameters {
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)
}
/// 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<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
}
}

View File

@@ -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<Item = SecNonce>,
aggnonces: impl IntoIterator<Item = &'a AggNonce>,
mut secnonces: BTreeMap<Outcome, SecNonce>,
aggnonces: &BTreeMap<Outcome, AggNonce>,
) -> Result<BTreeMap<Outcome, PartialSignature>, 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::<Outcome, PartialSignature>::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)?;

View File

@@ -27,14 +27,6 @@ impl SplitTransactionBuildOutput {
pub(crate) fn split_txs(&self) -> &BTreeMap<Outcome, Transaction> {
&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<Item = SecNonce>,
aggnonces: impl IntoIterator<Item = &'a AggNonce>,
mut secnonces: BTreeMap<WinCondition, SecNonce>,
aggnonces: &BTreeMap<WinCondition, AggNonce>,
) -> Result<BTreeMap<WinCondition, PartialSignature>, 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()

View File

@@ -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<TicketedDLC, Error> {
let outcome_tx_build = build_outcome_txs(&params, funding_outpoint)?;
let split_tx_build = build_split_txs(&params, &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<SecNonce>,
our_public_nonces: SigMap<PubNonce>,
}
/// 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<Point, SigMap<PubNonce>>,
aggregated_nonces: SigMap<AggNonce>,
our_partial_signatures: SigMap<PartialSignature>,
}
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<S: SigningSessionState> {
dlc: TicketedDLC,
state: S,
}
impl<S: SigningSessionState> SigningSession<S> {
/// Return a reference to the [`TicketedDLC`] inside this signing session.
pub fn dlc(&self) -> &TicketedDLC {
&self.dlc
}
}
impl SigningSession<NonceSharingRound> {
pub fn new<R: rand::RngCore + rand::CryptoRng>(
dlc: TicketedDLC,
mut rng: &mut R,
signing_key: impl Into<Scalar>,
) -> Result<SigningSession<NonceSharingRound>, 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<PubNonce> {
&self.state.our_public_nonces
}
/// Receive the nonces from all other signers.
pub fn compute_all_signatures(
self,
mut received_nonces: BTreeMap<Point, SigMap<PubNonce>>,
) -> Result<SigningSession<PartialSignatureSharingRound>, 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<Point, SigMap<()>> = received_nonces
.keys()
.map(|&key| Ok((key, self.dlc.params.sigmap_for_pubkey(key).ok_or(Error)?)))
.collect::<Result<_, Error>>()?;
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<AggNonce> = self.dlc.params.full_sigmap().map(
|outcome, _| {
received_nonces
.values()
.filter_map(|nonce_sigmap| nonce_sigmap.by_outcome.get(&outcome))
.sum::<AggNonce>()
},
|win_cond, _| {
received_nonces
.values()
.filter_map(|nonce_sigmap| nonce_sigmap.by_win_condition.get(&win_cond))
.sum::<AggNonce>()
},
);
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)
}
}