mirror of
https://github.com/conduition/dlctix.git
synced 2026-01-30 05:05:06 +01:00
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:
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -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",
|
||||
]
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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()
|
||||
|
||||
218
src/lib.rs
218
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<TicketedDLC, Error> {
|
||||
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<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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user