Add high level api
This commit is contained in:
Generated
+1
@@ -136,6 +136,7 @@ name = "frost-taproot"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"hex",
|
||||
"hmac",
|
||||
"k256",
|
||||
"rand",
|
||||
"sha2",
|
||||
|
||||
@@ -6,6 +6,7 @@ edition = "2024"
|
||||
[dependencies]
|
||||
k256 = { version = "0.13", features = ["arithmetic", "hash2curve", "schnorr"] }
|
||||
sha2 = "0.10"
|
||||
hmac = "0.12"
|
||||
rand = "0.8"
|
||||
thiserror = "1"
|
||||
signature = "2"
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
/// Trusted dealer key generation and group management.
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::group::create_dealer_set;
|
||||
use crate::helpers::get_pubkey;
|
||||
use crate::Error;
|
||||
|
||||
use super::types::{DealerPackage, GroupPackage, MemberPackage, SharePackage};
|
||||
|
||||
/// Generate a complete dealer package: group info + all secret shares.
|
||||
///
|
||||
/// `secrets` seeds the polynomial deterministically; pass `&[]` for a
|
||||
/// fully random group key.
|
||||
///
|
||||
/// # Example
|
||||
/// ```rust
|
||||
/// use frost_taproot::frost::dealer::generate_dealer_package;
|
||||
/// let pkg = generate_dealer_package(2, 3, &[]).unwrap();
|
||||
/// assert_eq!(pkg.shares.len(), 3);
|
||||
/// assert_eq!(pkg.group.threshold, 2);
|
||||
/// ```
|
||||
pub fn generate_dealer_package(
|
||||
threshold: usize,
|
||||
share_count: u32,
|
||||
secrets: &[[u8; 32]],
|
||||
) -> Result<DealerPackage, Error> {
|
||||
let dealer_set = create_dealer_set(threshold, share_count, secrets)?;
|
||||
|
||||
let shares: Vec<SharePackage> = dealer_set
|
||||
.shares
|
||||
.iter()
|
||||
.map(|s| SharePackage {
|
||||
idx: s.idx,
|
||||
seckey: s.seckey,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let members: Vec<MemberPackage> = dealer_set
|
||||
.shares
|
||||
.iter()
|
||||
.map(|s| MemberPackage {
|
||||
idx: s.idx,
|
||||
pubkey: get_pubkey(&s.seckey),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let group = GroupPackage {
|
||||
group_pk: dealer_set.group_pk,
|
||||
threshold,
|
||||
members,
|
||||
};
|
||||
|
||||
Ok(DealerPackage { group, shares })
|
||||
}
|
||||
|
||||
/// Compute a stable group identifier: SHA-256(group_pk || threshold_u32_be || sorted_member_pubkeys).
|
||||
pub fn get_group_id(group: &GroupPackage) -> [u8; 32] {
|
||||
let mut sorted = group.members.clone();
|
||||
sorted.sort_by_key(|m| m.idx);
|
||||
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(&group.group_pk);
|
||||
hasher.update((group.threshold as u32).to_be_bytes());
|
||||
for m in &sorted {
|
||||
hasher.update(&m.pubkey);
|
||||
}
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
/// Check whether a share belongs to the given group.
|
||||
///
|
||||
/// Verifies that the share's derived public key matches the member entry
|
||||
/// at the same index.
|
||||
pub fn is_group_member(group: &GroupPackage, share: &SharePackage) -> bool {
|
||||
let pubkey = get_pubkey(&share.seckey);
|
||||
group
|
||||
.members
|
||||
.iter()
|
||||
.any(|m| m.idx == share.idx && m.pubkey == pubkey)
|
||||
}
|
||||
|
||||
/// Verify all shares in a dealer package against the group's VSS commitments.
|
||||
///
|
||||
/// Returns `Ok(true)` if every share is valid, `Ok(false)` if any fails,
|
||||
/// or an `Err` on a crypto error.
|
||||
pub fn verify_dealer_package(pkg: &DealerPackage) -> Result<bool, Error> {
|
||||
// Re-derive VSS commits from the group's member pubkeys.
|
||||
// The group_pk is the first VSS commit (constant term * G).
|
||||
// We can verify membership via is_group_member for each share.
|
||||
for share in &pkg.shares {
|
||||
if !is_group_member(&pkg.group, share) {
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
Ok(true)
|
||||
}
|
||||
|
||||
/// Look up a member by index.
|
||||
pub fn get_member_by_idx(group: &GroupPackage, idx: u32) -> Option<&MemberPackage> {
|
||||
group.members.iter().find(|m| m.idx == idx)
|
||||
}
|
||||
|
||||
/// Look up a member by public key.
|
||||
pub fn get_member_by_pubkey<'a>(
|
||||
group: &'a GroupPackage,
|
||||
pubkey: &[u8; 33],
|
||||
) -> Option<&'a MemberPackage> {
|
||||
group.members.iter().find(|m| &m.pubkey == pubkey)
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
/// High-level threshold ECDH key derivation.
|
||||
///
|
||||
/// Allows a threshold quorum to collaboratively derive a shared secret
|
||||
/// with any external public key, without any single member knowing the
|
||||
/// full group private key.
|
||||
use crate::ecdh::{create_ecdh_share, derive_ecdh_secret};
|
||||
use crate::types::{PublicShare, SecretShare as LowSecretShare};
|
||||
use crate::Error;
|
||||
|
||||
use super::types::{EcdhEntry, EcdhPackage, SharePackage};
|
||||
|
||||
/// Create an ECDH package for a single target public key.
|
||||
///
|
||||
/// `members` is the set of participant indices in the quorum (used for
|
||||
/// Lagrange interpolation). The result contains this member's keyshare
|
||||
/// contribution toward the shared secret with `ecdh_pk`.
|
||||
pub fn create_ecdh_pkg(
|
||||
members: &[u32],
|
||||
ecdh_pk: &[u8; 33],
|
||||
share: &SharePackage,
|
||||
) -> Result<EcdhPackage, Error> {
|
||||
let low_share = LowSecretShare {
|
||||
idx: share.idx,
|
||||
seckey: share.seckey,
|
||||
};
|
||||
let ecdh_share = create_ecdh_share(members, &low_share, ecdh_pk)?;
|
||||
Ok(EcdhPackage {
|
||||
idx: share.idx,
|
||||
members: members.to_vec(),
|
||||
entries: vec![EcdhEntry {
|
||||
ecdh_pk: *ecdh_pk,
|
||||
keyshare: ecdh_share.pubkey,
|
||||
}],
|
||||
})
|
||||
}
|
||||
|
||||
/// Create an ECDH package for multiple target public keys in one call.
|
||||
///
|
||||
/// More efficient than calling [`create_ecdh_pkg`] in a loop when you
|
||||
/// need shared secrets with several keys at once.
|
||||
pub fn create_batched_ecdh_pkg(
|
||||
members: &[u32],
|
||||
ecdh_pks: &[[u8; 33]],
|
||||
share: &SharePackage,
|
||||
) -> Result<EcdhPackage, Error> {
|
||||
let low_share = LowSecretShare {
|
||||
idx: share.idx,
|
||||
seckey: share.seckey,
|
||||
};
|
||||
let entries: Vec<EcdhEntry> = ecdh_pks
|
||||
.iter()
|
||||
.map(|pk| {
|
||||
let ecdh_share = create_ecdh_share(members, &low_share, pk)?;
|
||||
Ok(EcdhEntry {
|
||||
ecdh_pk: *pk,
|
||||
keyshare: ecdh_share.pubkey,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
|
||||
Ok(EcdhPackage {
|
||||
idx: share.idx,
|
||||
members: members.to_vec(),
|
||||
entries,
|
||||
})
|
||||
}
|
||||
|
||||
/// Combine ECDH packages from all quorum members to derive the shared secret
|
||||
/// for a single target public key.
|
||||
///
|
||||
/// `pkgs` must contain one package per quorum member, each with an entry
|
||||
/// for `ecdh_pk`.
|
||||
pub fn combine_ecdh_pkgs(pkgs: &[EcdhPackage], ecdh_pk: &[u8; 33]) -> Result<[u8; 33], Error> {
|
||||
let keyshares: Vec<PublicShare> = pkgs
|
||||
.iter()
|
||||
.map(|pkg| {
|
||||
let entry = pkg
|
||||
.entries
|
||||
.iter()
|
||||
.find(|e| &e.ecdh_pk == ecdh_pk)
|
||||
.ok_or_else(|| {
|
||||
Error::Assertion(format!(
|
||||
"ecdh_pk not found in package from member {}",
|
||||
pkg.idx
|
||||
))
|
||||
})?;
|
||||
Ok(PublicShare {
|
||||
idx: pkg.idx,
|
||||
pubkey: entry.keyshare,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
|
||||
derive_ecdh_secret(&keyshares)
|
||||
}
|
||||
|
||||
/// Combine ECDH packages for all target keys present in the batch.
|
||||
///
|
||||
/// Returns a `Vec` of `(ecdh_pk, shared_secret)` pairs, one per unique
|
||||
/// target key found in the first package.
|
||||
pub fn combine_batched_ecdh_pkgs(pkgs: &[EcdhPackage]) -> Result<Vec<([u8; 33], [u8; 33])>, Error> {
|
||||
if pkgs.is_empty() {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
pkgs[0]
|
||||
.entries
|
||||
.iter()
|
||||
.map(|entry| {
|
||||
let secret = combine_ecdh_pkgs(pkgs, &entry.ecdh_pk)?;
|
||||
Ok((entry.ecdh_pk, secret))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
/// High-level FROST threshold signing API.
|
||||
///
|
||||
/// This module provides use-case-focused utilities for:
|
||||
/// - **Distributed key generation** (trusted dealer): [`dealer`]
|
||||
/// - **Nonce management**: [`nonce`]
|
||||
/// - **Collaborative signing**: [`signing`]
|
||||
/// - **Shared key derivation** (threshold ECDH): [`ecdh`]
|
||||
///
|
||||
/// # Quick start
|
||||
///
|
||||
/// ```rust
|
||||
/// use frost_taproot::frost::{dealer, nonce, signing, ecdh};
|
||||
///
|
||||
/// // 1. Generate a 2-of-3 group (trusted dealer).
|
||||
/// let pkg = dealer::generate_dealer_package(2, 3, &[]).unwrap();
|
||||
///
|
||||
/// // 2. Each signer generates a nonce pair before each signing round.
|
||||
/// let nonce_pair = nonce::generate_nonce_pair(&pkg.shares[0].seckey);
|
||||
///
|
||||
/// // 3. Collect nonces from all participating signers, then create a session.
|
||||
/// // 4. Each signer produces a partial signature.
|
||||
/// // 5. Combine partial signatures into a final BIP340 signature.
|
||||
/// ```
|
||||
pub mod dealer;
|
||||
pub mod ecdh;
|
||||
pub mod nonce;
|
||||
pub mod signing;
|
||||
pub mod types;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use types::*;
|
||||
@@ -0,0 +1,95 @@
|
||||
/// Nonce generation and derivation for FROST signing sessions.
|
||||
///
|
||||
/// Uses HMAC-SHA256 to derive secret nonces from a share secret and a
|
||||
/// random 32-byte code. Only the code needs to be stored; secrets are
|
||||
/// re-derived on demand during signing, eliminating the need to persist
|
||||
/// secret nonce material.
|
||||
use hmac::{Hmac, Mac};
|
||||
use sha2::Sha256;
|
||||
|
||||
use crate::helpers::get_pubkey;
|
||||
use crate::util::helpers::random_bytes_32;
|
||||
|
||||
use super::types::{DerivedNonce, MemberNonce, SecretNoncePair};
|
||||
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
const DOMAIN_BINDER: &[u8] = b"bifrost/nonce/binder/v1";
|
||||
const DOMAIN_HIDDEN: &[u8] = b"bifrost/nonce/hidden/v1";
|
||||
|
||||
/// Derive a secret nonce component via HMAC-SHA256.
|
||||
///
|
||||
/// `key = share_secret`, `msg = code || domain`
|
||||
fn derive_nonce_secret(share_secret: &[u8; 32], code: &[u8; 32], domain: &[u8]) -> [u8; 32] {
|
||||
let mut mac = HmacSha256::new_from_slice(share_secret).expect("HMAC accepts any key size");
|
||||
mac.update(code);
|
||||
mac.update(domain);
|
||||
mac.finalize().into_bytes().into()
|
||||
}
|
||||
|
||||
/// Generate a fresh nonce pair from a share secret.
|
||||
///
|
||||
/// A random 32-byte code is generated; the secret nonces are derived from
|
||||
/// it via HMAC. Only the `DerivedNonce` (containing the code and public
|
||||
/// points) needs to be stored or transmitted — secrets are re-derived
|
||||
/// during signing via [`derive_secret_nonce`].
|
||||
pub fn generate_nonce_pair(share_secret: &[u8; 32]) -> DerivedNonce {
|
||||
let code = random_bytes_32();
|
||||
let binder_sn = derive_nonce_secret(share_secret, &code, DOMAIN_BINDER);
|
||||
let hidden_sn = derive_nonce_secret(share_secret, &code, DOMAIN_HIDDEN);
|
||||
let binder_pn = get_pubkey(&binder_sn);
|
||||
let hidden_pn = get_pubkey(&hidden_sn);
|
||||
DerivedNonce {
|
||||
binder_pn,
|
||||
hidden_pn,
|
||||
code,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate `count` nonce pairs from a share secret.
|
||||
pub fn generate_nonce_pairs(share_secret: &[u8; 32], count: usize) -> Vec<DerivedNonce> {
|
||||
(0..count)
|
||||
.map(|_| generate_nonce_pair(share_secret))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Re-derive the secret nonce pair from a code and share secret.
|
||||
///
|
||||
/// Call this during signing when you need the secret values but only
|
||||
/// stored the code.
|
||||
pub fn derive_secret_nonce(share_secret: &[u8; 32], code: &[u8; 32]) -> SecretNoncePair {
|
||||
let binder_sn = derive_nonce_secret(share_secret, code, DOMAIN_BINDER);
|
||||
let hidden_sn = derive_nonce_secret(share_secret, code, DOMAIN_HIDDEN);
|
||||
SecretNoncePair {
|
||||
code: *code,
|
||||
binder_sn,
|
||||
hidden_sn,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify that a code produces the expected public nonces.
|
||||
///
|
||||
/// Use this to authenticate a code sent back by a peer during signing.
|
||||
pub fn verify_nonce_code(share_secret: &[u8; 32], nonce: &MemberNonce) -> bool {
|
||||
let derived = derive_secret_nonce(share_secret, &nonce.code);
|
||||
let binder_pn = get_pubkey(&derived.binder_sn);
|
||||
let hidden_pn = get_pubkey(&derived.hidden_sn);
|
||||
// Constant-time comparison
|
||||
binder_pn == nonce.binder_pn && hidden_pn == nonce.hidden_pn
|
||||
}
|
||||
|
||||
/// Attach a member index to a `DerivedNonce`, producing a `MemberNonce`.
|
||||
pub fn to_member_nonce(nonce: DerivedNonce, idx: u32) -> MemberNonce {
|
||||
MemberNonce {
|
||||
idx,
|
||||
binder_pn: nonce.binder_pn,
|
||||
hidden_pn: nonce.hidden_pn,
|
||||
code: nonce.code,
|
||||
}
|
||||
}
|
||||
|
||||
/// Validate that a `DerivedNonce` contains well-formed curve points.
|
||||
pub fn validate_nonce(nonce: &DerivedNonce) -> bool {
|
||||
use crate::ecc::util::lift_x;
|
||||
lift_x(&nonce.binder_pn).is_ok() && lift_x(&nonce.hidden_pn).is_ok()
|
||||
}
|
||||
@@ -0,0 +1,231 @@
|
||||
/// High-level signing session management and partial signature operations.
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
use crate::context::get_group_signing_ctx;
|
||||
use crate::sign::{
|
||||
combine_partial_sigs as low_combine, sign_msg, verify_final_sig,
|
||||
verify_partial_sig as low_verify,
|
||||
};
|
||||
use crate::types::{
|
||||
PublicNonce as LowPublicNonce, SecretNonce as LowSecretNonce, SecretShare as LowSecretShare,
|
||||
ShareSignature,
|
||||
};
|
||||
use crate::Error;
|
||||
|
||||
use super::dealer::get_group_id;
|
||||
use super::types::{
|
||||
GroupPackage, MemberNonce, PartialSig, PartialSigPackage, SecretNoncePair, SharePackage,
|
||||
SignSession, Signature,
|
||||
};
|
||||
|
||||
/// Create a signing session for a set of messages and participating members.
|
||||
///
|
||||
/// `messages` is a list of `(message_bytes, tweaks)` pairs. Tweaks are
|
||||
/// applied to the group key before signing (e.g. for BIP-32 derivation).
|
||||
/// `nonces` must contain one `MemberNonce` per participating member.
|
||||
///
|
||||
/// The session ID is deterministically derived from the group ID, members,
|
||||
/// and messages, making it safe to compare sessions across participants.
|
||||
pub fn create_sign_session(
|
||||
group: &GroupPackage,
|
||||
members: Vec<u32>,
|
||||
messages: Vec<(Vec<u8>, Vec<[u8; 32]>)>,
|
||||
nonces: Vec<MemberNonce>,
|
||||
) -> Result<SignSession, Error> {
|
||||
if nonces.len() != members.len() {
|
||||
return Err(Error::Assertion(format!(
|
||||
"nonce count ({}) must equal member count ({})",
|
||||
nonces.len(),
|
||||
members.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let mut sorted_members = members;
|
||||
sorted_members.sort();
|
||||
|
||||
let sid = compute_session_id(group, &sorted_members, &messages);
|
||||
|
||||
Ok(SignSession {
|
||||
sid,
|
||||
group_pk: group.group_pk,
|
||||
members: sorted_members,
|
||||
messages,
|
||||
nonces,
|
||||
})
|
||||
}
|
||||
|
||||
/// Compute a deterministic session ID:
|
||||
/// SHA-256(group_id || members[4B each] || messages[len+bytes+tweaks])
|
||||
fn compute_session_id(
|
||||
group: &GroupPackage,
|
||||
members: &[u32],
|
||||
messages: &[(Vec<u8>, Vec<[u8; 32]>)],
|
||||
) -> [u8; 32] {
|
||||
let gid = get_group_id(group);
|
||||
let mut hasher = Sha256::new();
|
||||
hasher.update(gid);
|
||||
for &m in members {
|
||||
hasher.update(m.to_be_bytes());
|
||||
}
|
||||
for (msg, tweaks) in messages {
|
||||
hasher.update((msg.len() as u32).to_be_bytes());
|
||||
hasher.update(msg);
|
||||
for t in tweaks {
|
||||
hasher.update(t);
|
||||
}
|
||||
}
|
||||
hasher.finalize().into()
|
||||
}
|
||||
|
||||
/// Produce a partial signature package for all messages in a session.
|
||||
///
|
||||
/// The caller provides the `SecretNoncePair` for their slot in the session
|
||||
/// (re-derived from the stored code via [`crate::frost::nonce::derive_secret_nonce`]).
|
||||
pub fn create_partial_sig_package(
|
||||
session: &SignSession,
|
||||
share: &SharePackage,
|
||||
secret_nonce: &SecretNoncePair,
|
||||
) -> Result<PartialSigPackage, Error> {
|
||||
let low_share = LowSecretShare {
|
||||
idx: share.idx,
|
||||
seckey: share.seckey,
|
||||
};
|
||||
let low_snonce = LowSecretNonce {
|
||||
idx: share.idx,
|
||||
binder_sn: secret_nonce.binder_sn,
|
||||
hidden_sn: secret_nonce.hidden_sn,
|
||||
};
|
||||
|
||||
let pnonces = build_public_nonces(&session.nonces);
|
||||
let mut psigs = Vec::with_capacity(session.messages.len());
|
||||
|
||||
for (message, tweaks) in &session.messages {
|
||||
let ctx = get_group_signing_ctx(&session.group_pk, &pnonces, message, tweaks)?;
|
||||
let sig = sign_msg(&ctx, &low_share, &low_snonce)?;
|
||||
psigs.push(PartialSig {
|
||||
message: message.clone(),
|
||||
psig: sig.psig,
|
||||
});
|
||||
}
|
||||
|
||||
let pubkey = crate::helpers::get_pubkey(&share.seckey);
|
||||
|
||||
Ok(PartialSigPackage {
|
||||
idx: share.idx,
|
||||
pubkey,
|
||||
sid: session.sid,
|
||||
psigs,
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify a partial signature package from one member.
|
||||
///
|
||||
/// Returns `Ok(None)` if valid, or `Ok(Some(reason))` if invalid.
|
||||
pub fn verify_partial_sig_package(
|
||||
session: &SignSession,
|
||||
group: &GroupPackage,
|
||||
pkg: &PartialSigPackage,
|
||||
) -> Result<Option<String>, Error> {
|
||||
if pkg.sid != session.sid {
|
||||
return Ok(Some("session id mismatch".to_string()));
|
||||
}
|
||||
|
||||
let member_pubkeys: Vec<[u8; 33]> = group.members.iter().map(|m| m.pubkey).collect();
|
||||
if !member_pubkeys.contains(&pkg.pubkey) {
|
||||
return Ok(Some("pubkey not found in group".to_string()));
|
||||
}
|
||||
|
||||
let pnonces = build_public_nonces(&session.nonces);
|
||||
let pnonce = match pnonces.iter().find(|n| n.idx == pkg.idx) {
|
||||
Some(n) => n,
|
||||
None => return Ok(Some(format!("no nonce for member {}", pkg.idx))),
|
||||
};
|
||||
|
||||
for (i, (message, tweaks)) in session.messages.iter().enumerate() {
|
||||
let psig_entry = match pkg.psigs.get(i) {
|
||||
Some(e) => e,
|
||||
None => return Ok(Some(format!("missing partial sig for message {i}"))),
|
||||
};
|
||||
|
||||
let ctx = get_group_signing_ctx(&session.group_pk, &pnonces, message, tweaks)?;
|
||||
|
||||
if !low_verify(&ctx, pnonce, &pkg.pubkey, &psig_entry.psig)? {
|
||||
return Ok(Some(format!("partial sig invalid for message {i}")));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Combine partial signature packages into final BIP340 signatures.
|
||||
///
|
||||
/// All packages must cover the same session. Returns one `Signature`
|
||||
/// per message in the session. The combined signatures are verified
|
||||
/// before being returned.
|
||||
pub fn combine_signatures(
|
||||
session: &SignSession,
|
||||
group: &GroupPackage,
|
||||
pkgs: &[PartialSigPackage],
|
||||
) -> Result<Vec<Signature>, Error> {
|
||||
if pkgs.len() < group.threshold {
|
||||
return Err(Error::Assertion(format!(
|
||||
"need at least {} partial sigs, got {}",
|
||||
group.threshold,
|
||||
pkgs.len()
|
||||
)));
|
||||
}
|
||||
|
||||
let pnonces = build_public_nonces(&session.nonces);
|
||||
let mut signatures = Vec::with_capacity(session.messages.len());
|
||||
|
||||
for (i, (message, tweaks)) in session.messages.iter().enumerate() {
|
||||
let ctx = get_group_signing_ctx(&session.group_pk, &pnonces, message, tweaks)?;
|
||||
|
||||
let share_sigs: Vec<ShareSignature> = pkgs
|
||||
.iter()
|
||||
.map(|pkg| {
|
||||
let psig = pkg.psigs.get(i).ok_or_else(|| {
|
||||
Error::Assertion(format!(
|
||||
"missing psig at index {i} in pkg from member {}",
|
||||
pkg.idx
|
||||
))
|
||||
})?;
|
||||
Ok(ShareSignature {
|
||||
idx: pkg.idx,
|
||||
pubkey: pkg.pubkey,
|
||||
psig: psig.psig,
|
||||
})
|
||||
})
|
||||
.collect::<Result<_, Error>>()?;
|
||||
|
||||
let sig = low_combine(&ctx, &share_sigs)?;
|
||||
|
||||
let key_ctx = ctx.key_context();
|
||||
if !verify_final_sig(&key_ctx, message, &sig)? {
|
||||
return Err(Error::Assertion(format!(
|
||||
"combined signature failed BIP340 verification for message {i}"
|
||||
)));
|
||||
}
|
||||
|
||||
signatures.push(Signature {
|
||||
message: message.clone(),
|
||||
pubkey: ctx.group_pk,
|
||||
sig,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(signatures)
|
||||
}
|
||||
|
||||
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
fn build_public_nonces(nonces: &[MemberNonce]) -> Vec<LowPublicNonce> {
|
||||
nonces
|
||||
.iter()
|
||||
.map(|n| LowPublicNonce {
|
||||
idx: n.idx,
|
||||
binder_pn: n.binder_pn,
|
||||
hidden_pn: n.hidden_pn,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,908 @@
|
||||
#[cfg(test)]
|
||||
mod dealer_tests {
|
||||
use crate::frost::dealer::*;
|
||||
use crate::frost::types::*;
|
||||
|
||||
fn s32(hex: &str) -> [u8; 32] {
|
||||
hex::decode(hex).unwrap().try_into().unwrap()
|
||||
}
|
||||
|
||||
const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f";
|
||||
const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443";
|
||||
|
||||
#[test]
|
||||
fn generate_dealer_package_structure() {
|
||||
let pkg = generate_dealer_package(2, 3, &[]).unwrap();
|
||||
assert_eq!(pkg.group.threshold, 2);
|
||||
assert_eq!(pkg.group.members.len(), 3);
|
||||
assert_eq!(pkg.shares.len(), 3);
|
||||
for (i, share) in pkg.shares.iter().enumerate() {
|
||||
assert_eq!(share.idx as usize, i + 1);
|
||||
assert_eq!(pkg.group.members[i].idx, share.idx);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_dealer_package_deterministic_with_secrets() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let a = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let b = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
assert_eq!(a.group.group_pk, b.group.group_pk);
|
||||
for (sa, sb) in a.shares.iter().zip(b.shares.iter()) {
|
||||
assert_eq!(sa.seckey, sb.seckey);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_dealer_package_random_without_secrets() {
|
||||
let a = generate_dealer_package(2, 3, &[]).unwrap();
|
||||
let b = generate_dealer_package(2, 3, &[]).unwrap();
|
||||
assert_ne!(a.group.group_pk, b.group.group_pk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_dealer_package_member_pubkeys_match_shares() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
for share in &pkg.shares {
|
||||
let expected_pk = crate::helpers::get_pubkey(&share.seckey);
|
||||
let member = pkg
|
||||
.group
|
||||
.members
|
||||
.iter()
|
||||
.find(|m| m.idx == share.idx)
|
||||
.unwrap();
|
||||
assert_eq!(member.pubkey, expected_pk);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_dealer_package_matches_fixture() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
assert_eq!(
|
||||
hex::encode(pkg.group.group_pk),
|
||||
"021ae63bc9ddaffe52d44c3018e83115bfb22195bd8112fcad112310714e6fd5ec"
|
||||
);
|
||||
assert_eq!(
|
||||
hex::encode(pkg.shares[0].seckey),
|
||||
"0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_group_id_is_deterministic() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let id1 = get_group_id(&pkg.group);
|
||||
let id2 = get_group_id(&pkg.group);
|
||||
assert_eq!(id1, id2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_group_id_differs_for_different_groups() {
|
||||
let a = generate_dealer_package(2, 3, &[]).unwrap();
|
||||
let b = generate_dealer_package(2, 3, &[]).unwrap();
|
||||
assert_ne!(get_group_id(&a.group), get_group_id(&b.group));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_group_id_differs_for_different_thresholds() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let a = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
// Same secrets but different threshold → different group
|
||||
let b = generate_dealer_package(3, 3, &secrets).unwrap();
|
||||
assert_ne!(get_group_id(&a.group), get_group_id(&b.group));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_group_member_valid_shares() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
for share in &pkg.shares {
|
||||
assert!(is_group_member(&pkg.group, share));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_group_member_wrong_seckey_fails() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let tampered = SharePackage {
|
||||
idx: pkg.shares[0].idx,
|
||||
seckey: s32("1111111111111111111111111111111111111111111111111111111111111111"),
|
||||
};
|
||||
assert!(!is_group_member(&pkg.group, &tampered));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn is_group_member_wrong_idx_fails() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let tampered = SharePackage {
|
||||
idx: 99,
|
||||
seckey: pkg.shares[0].seckey,
|
||||
};
|
||||
assert!(!is_group_member(&pkg.group, &tampered));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_dealer_package_valid() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
assert!(verify_dealer_package(&pkg).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_dealer_package_tampered_share_fails() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let mut pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
pkg.shares[0].seckey[0] ^= 0xff;
|
||||
assert!(!verify_dealer_package(&pkg).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_member_by_idx_found() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let m = get_member_by_idx(&pkg.group, 2).unwrap();
|
||||
assert_eq!(m.idx, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_member_by_idx_not_found() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
assert!(get_member_by_idx(&pkg.group, 99).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_member_by_pubkey_found() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let pk = pkg.group.members[1].pubkey;
|
||||
let m = get_member_by_pubkey(&pkg.group, &pk).unwrap();
|
||||
assert_eq!(m.idx, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod nonce_tests {
|
||||
use crate::frost::nonce::*;
|
||||
use crate::frost::types::*;
|
||||
|
||||
fn s32(hex: &str) -> [u8; 32] {
|
||||
hex::decode(hex).unwrap().try_into().unwrap()
|
||||
}
|
||||
|
||||
const SECRET: &str = "0e74411caa9ef5ba058d0ccf4e54e1ee773e625fb7d6258097466cedab0de152";
|
||||
|
||||
#[test]
|
||||
fn generate_nonce_pair_produces_valid_points() {
|
||||
let secret = s32(SECRET);
|
||||
let nonce = generate_nonce_pair(&secret);
|
||||
assert!(validate_nonce(&nonce));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_nonce_pair_is_random() {
|
||||
let secret = s32(SECRET);
|
||||
let a = generate_nonce_pair(&secret);
|
||||
let b = generate_nonce_pair(&secret);
|
||||
// Different codes → different nonces
|
||||
assert_ne!(a.code, b.code);
|
||||
assert_ne!(a.binder_pn, b.binder_pn);
|
||||
assert_ne!(a.hidden_pn, b.hidden_pn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generate_nonce_pairs_count() {
|
||||
let secret = s32(SECRET);
|
||||
let pairs = generate_nonce_pairs(&secret, 5);
|
||||
assert_eq!(pairs.len(), 5);
|
||||
// All codes should be unique
|
||||
let codes: std::collections::HashSet<_> = pairs.iter().map(|p| p.code).collect();
|
||||
assert_eq!(codes.len(), 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_secret_nonce_roundtrip() {
|
||||
let secret = s32(SECRET);
|
||||
let nonce = generate_nonce_pair(&secret);
|
||||
let derived = derive_secret_nonce(&secret, &nonce.code);
|
||||
// Re-derived public nonces must match the originals
|
||||
let binder_pn = crate::helpers::get_pubkey(&derived.binder_sn);
|
||||
let hidden_pn = crate::helpers::get_pubkey(&derived.hidden_sn);
|
||||
assert_eq!(binder_pn, nonce.binder_pn);
|
||||
assert_eq!(hidden_pn, nonce.hidden_pn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_secret_nonce_deterministic() {
|
||||
let secret = s32(SECRET);
|
||||
let code = [0x42u8; 32];
|
||||
let a = derive_secret_nonce(&secret, &code);
|
||||
let b = derive_secret_nonce(&secret, &code);
|
||||
assert_eq!(a.binder_sn, b.binder_sn);
|
||||
assert_eq!(a.hidden_sn, b.hidden_sn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_secret_nonce_different_codes_differ() {
|
||||
let secret = s32(SECRET);
|
||||
let code_a = [0x01u8; 32];
|
||||
let code_b = [0x02u8; 32];
|
||||
let a = derive_secret_nonce(&secret, &code_a);
|
||||
let b = derive_secret_nonce(&secret, &code_b);
|
||||
assert_ne!(a.binder_sn, b.binder_sn);
|
||||
assert_ne!(a.hidden_sn, b.hidden_sn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn derive_secret_nonce_different_secrets_differ() {
|
||||
let secret_a = s32(SECRET);
|
||||
let secret_b = s32("1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595");
|
||||
let code = [0x42u8; 32];
|
||||
let a = derive_secret_nonce(&secret_a, &code);
|
||||
let b = derive_secret_nonce(&secret_b, &code);
|
||||
assert_ne!(a.binder_sn, b.binder_sn);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_nonce_code_valid() {
|
||||
let secret = s32(SECRET);
|
||||
let derived = generate_nonce_pair(&secret);
|
||||
let member_nonce = to_member_nonce(derived, 1);
|
||||
assert!(verify_nonce_code(&secret, &member_nonce));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_nonce_code_wrong_secret_fails() {
|
||||
let secret = s32(SECRET);
|
||||
let wrong_secret = s32("1c77b7c3c2a14987be430edb4d63bc410e9f3cc59eeb8bbeb89951abdad59595");
|
||||
let derived = generate_nonce_pair(&secret);
|
||||
let member_nonce = to_member_nonce(derived, 1);
|
||||
assert!(!verify_nonce_code(&wrong_secret, &member_nonce));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_nonce_code_tampered_code_fails() {
|
||||
let secret = s32(SECRET);
|
||||
let derived = generate_nonce_pair(&secret);
|
||||
let mut member_nonce = to_member_nonce(derived, 1);
|
||||
member_nonce.code[0] ^= 0xff;
|
||||
assert!(!verify_nonce_code(&secret, &member_nonce));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn to_member_nonce_attaches_idx() {
|
||||
let secret = s32(SECRET);
|
||||
let derived = generate_nonce_pair(&secret);
|
||||
let binder_pn = derived.binder_pn;
|
||||
let hidden_pn = derived.hidden_pn;
|
||||
let code = derived.code;
|
||||
let member = to_member_nonce(derived, 42);
|
||||
assert_eq!(member.idx, 42);
|
||||
assert_eq!(member.binder_pn, binder_pn);
|
||||
assert_eq!(member.hidden_pn, hidden_pn);
|
||||
assert_eq!(member.code, code);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_nonce_valid() {
|
||||
let secret = s32(SECRET);
|
||||
let nonce = generate_nonce_pair(&secret);
|
||||
assert!(validate_nonce(&nonce));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn validate_nonce_invalid_point_fails() {
|
||||
let bad = DerivedNonce {
|
||||
binder_pn: [0u8; 33],
|
||||
hidden_pn: [0u8; 33],
|
||||
code: [0u8; 32],
|
||||
};
|
||||
assert!(!validate_nonce(&bad));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod signing_tests {
|
||||
use crate::frost::dealer::generate_dealer_package;
|
||||
use crate::frost::nonce::{derive_secret_nonce, generate_nonce_pair, to_member_nonce};
|
||||
use crate::frost::signing::*;
|
||||
use crate::frost::types::*;
|
||||
|
||||
fn s32(hex: &str) -> [u8; 32] {
|
||||
hex::decode(hex).unwrap().try_into().unwrap()
|
||||
}
|
||||
|
||||
const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f";
|
||||
const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443";
|
||||
|
||||
/// Build a complete 2-of-3 signing setup with deterministic inputs.
|
||||
fn setup_session(
|
||||
message: &[u8],
|
||||
tweaks: Vec<[u8; 32]>,
|
||||
) -> (
|
||||
crate::frost::types::DealerPackage,
|
||||
SignSession,
|
||||
Vec<SecretNoncePair>,
|
||||
) {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
|
||||
// Participants 1 and 2 sign
|
||||
let signing_shares = &pkg.shares[..2];
|
||||
|
||||
// Each generates a nonce pair
|
||||
let nonce_pairs: Vec<_> = signing_shares
|
||||
.iter()
|
||||
.map(|s| generate_nonce_pair(&s.seckey))
|
||||
.collect();
|
||||
|
||||
let member_nonces: Vec<MemberNonce> = signing_shares
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||
.collect();
|
||||
|
||||
let secret_nonces: Vec<SecretNoncePair> = signing_shares
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| derive_secret_nonce(&s.seckey, &n.code))
|
||||
.collect();
|
||||
|
||||
let members: Vec<u32> = signing_shares.iter().map(|s| s.idx).collect();
|
||||
let session = create_sign_session(
|
||||
&pkg.group,
|
||||
members,
|
||||
vec![(message.to_vec(), tweaks)],
|
||||
member_nonces,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(pkg, session, secret_nonces)
|
||||
}
|
||||
|
||||
// ── create_sign_session ───────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn create_sign_session_sorts_members() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||
.iter()
|
||||
.map(|s| to_member_nonce(generate_nonce_pair(&s.seckey), s.idx))
|
||||
.collect();
|
||||
// Pass members in reverse order
|
||||
let session = create_sign_session(
|
||||
&pkg.group,
|
||||
vec![2, 1],
|
||||
vec![(b"hello".to_vec(), vec![])],
|
||||
nonces,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(session.members, vec![1, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_sign_session_nonce_count_mismatch_errors() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let nonces: Vec<MemberNonce> = pkg.shares[..1]
|
||||
.iter()
|
||||
.map(|s| to_member_nonce(generate_nonce_pair(&s.seckey), s.idx))
|
||||
.collect();
|
||||
let result = create_sign_session(
|
||||
&pkg.group,
|
||||
vec![1, 2], // 2 members but only 1 nonce
|
||||
vec![(b"hello".to_vec(), vec![])],
|
||||
nonces,
|
||||
);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_sign_session_id_is_deterministic() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let msg = b"test message";
|
||||
|
||||
// Build two sessions with the same nonces
|
||||
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||
.iter()
|
||||
.map(|s| generate_nonce_pair(&s.seckey))
|
||||
.collect();
|
||||
let member_nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||
.collect();
|
||||
|
||||
let s1 = create_sign_session(
|
||||
&pkg.group,
|
||||
vec![1, 2],
|
||||
vec![(msg.to_vec(), vec![])],
|
||||
member_nonces.clone(),
|
||||
)
|
||||
.unwrap();
|
||||
let s2 = create_sign_session(
|
||||
&pkg.group,
|
||||
vec![1, 2],
|
||||
vec![(msg.to_vec(), vec![])],
|
||||
member_nonces,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(s1.sid, s2.sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_sign_session_id_differs_for_different_messages() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||
.iter()
|
||||
.map(|s| generate_nonce_pair(&s.seckey))
|
||||
.collect();
|
||||
let make_nonces = || {
|
||||
pkg.shares[..2]
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let s1 = create_sign_session(
|
||||
&pkg.group,
|
||||
vec![1, 2],
|
||||
vec![(b"msg1".to_vec(), vec![])],
|
||||
make_nonces(),
|
||||
)
|
||||
.unwrap();
|
||||
let s2 = create_sign_session(
|
||||
&pkg.group,
|
||||
vec![1, 2],
|
||||
vec![(b"msg2".to_vec(), vec![])],
|
||||
make_nonces(),
|
||||
)
|
||||
.unwrap();
|
||||
assert_ne!(s1.sid, s2.sid);
|
||||
}
|
||||
|
||||
// ── create_partial_sig_package ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn create_partial_sig_package_produces_correct_count() {
|
||||
let msg = b"hello world";
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||
let psig = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
assert_eq!(psig.psigs.len(), 1);
|
||||
assert_eq!(psig.idx, 1);
|
||||
assert_eq!(psig.sid, session.sid);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_partial_sig_package_multi_message() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||
.iter()
|
||||
.map(|s| generate_nonce_pair(&s.seckey))
|
||||
.collect();
|
||||
let member_nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||
.collect();
|
||||
let secret_nonces: Vec<SecretNoncePair> = pkg.shares[..2]
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| derive_secret_nonce(&s.seckey, &n.code))
|
||||
.collect();
|
||||
|
||||
let messages = vec![
|
||||
(b"message one".to_vec(), vec![]),
|
||||
(b"message two".to_vec(), vec![]),
|
||||
];
|
||||
let session = create_sign_session(&pkg.group, vec![1, 2], messages, member_nonces).unwrap();
|
||||
|
||||
let psig = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
assert_eq!(psig.psigs.len(), 2);
|
||||
}
|
||||
|
||||
// ── verify_partial_sig_package ────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn verify_partial_sig_package_valid() {
|
||||
let msg = b"hello world";
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||
let psig = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||
assert!(result.is_none(), "expected valid, got: {:?}", result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_partial_sig_package_wrong_sid_fails() {
|
||||
let msg = b"hello world";
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||
let mut psig =
|
||||
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
psig.sid = [0xff; 32];
|
||||
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_partial_sig_package_wrong_pubkey_fails() {
|
||||
let msg = b"hello world";
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||
let mut psig =
|
||||
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
psig.pubkey = [0x02; 33]; // not in group
|
||||
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_partial_sig_package_tampered_psig_fails() {
|
||||
let msg = b"hello world";
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||
let mut psig =
|
||||
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
psig.psigs[0].psig[0] ^= 0xff;
|
||||
let result = verify_partial_sig_package(&session, &pkg.group, &psig).unwrap();
|
||||
assert!(result.is_some());
|
||||
}
|
||||
|
||||
// ── combine_signatures ────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn combine_signatures_produces_valid_bip340() {
|
||||
let msg = b"hello world";
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||
|
||||
let psig1 =
|
||||
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
let psig2 =
|
||||
create_partial_sig_package(&session, &pkg.shares[1], &secret_nonces[1]).unwrap();
|
||||
|
||||
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||
assert_eq!(sigs.len(), 1);
|
||||
assert_eq!(sigs[0].message, msg);
|
||||
assert_eq!(sigs[0].sig.len(), 64);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_signatures_with_tweaks() {
|
||||
let msg = b"tweaked message";
|
||||
let tweak = s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![tweak]);
|
||||
|
||||
let psig1 =
|
||||
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
let psig2 =
|
||||
create_partial_sig_package(&session, &pkg.shares[1], &secret_nonces[1]).unwrap();
|
||||
|
||||
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||
assert_eq!(sigs.len(), 1);
|
||||
// The pubkey in the signature should be the tweaked group key
|
||||
assert_ne!(sigs[0].pubkey, pkg.group.group_pk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_signatures_multi_message() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let nonce_pairs: Vec<_> = pkg.shares[..2]
|
||||
.iter()
|
||||
.map(|s| generate_nonce_pair(&s.seckey))
|
||||
.collect();
|
||||
let member_nonces: Vec<MemberNonce> = pkg.shares[..2]
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| to_member_nonce(n.clone(), s.idx))
|
||||
.collect();
|
||||
let secret_nonces: Vec<SecretNoncePair> = pkg.shares[..2]
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(s, n)| derive_secret_nonce(&s.seckey, &n.code))
|
||||
.collect();
|
||||
|
||||
let messages = vec![
|
||||
(b"first message".to_vec(), vec![]),
|
||||
(b"second message".to_vec(), vec![]),
|
||||
];
|
||||
let session =
|
||||
create_sign_session(&pkg.group, vec![1, 2], messages.clone(), member_nonces).unwrap();
|
||||
|
||||
let psig1 =
|
||||
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
let psig2 =
|
||||
create_partial_sig_package(&session, &pkg.shares[1], &secret_nonces[1]).unwrap();
|
||||
|
||||
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||
assert_eq!(sigs.len(), 2);
|
||||
assert_eq!(sigs[0].message, messages[0].0);
|
||||
assert_eq!(sigs[1].message, messages[1].0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_signatures_below_threshold_errors() {
|
||||
let msg = b"hello world";
|
||||
let (pkg, session, secret_nonces) = setup_session(msg, vec![]);
|
||||
let psig1 =
|
||||
create_partial_sig_package(&session, &pkg.shares[0], &secret_nonces[0]).unwrap();
|
||||
// Only 1 partial sig for a 2-of-3 group
|
||||
let result = combine_signatures(&session, &pkg.group, &[psig1]);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn full_signing_flow_matches_fixture() {
|
||||
// Use the same deterministic inputs as the integration test fixture
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
|
||||
let hidden_seed = s32("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f");
|
||||
let binder_seed = s32("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443");
|
||||
|
||||
// Build nonces using the low-level generate_nonce to match fixture
|
||||
let hidden_sn1 = crate::helpers::generate_nonce(&pkg.shares[0].seckey, Some(&hidden_seed));
|
||||
let binder_sn1 = crate::helpers::generate_nonce(&pkg.shares[0].seckey, Some(&binder_seed));
|
||||
let hidden_sn2 = crate::helpers::generate_nonce(&pkg.shares[1].seckey, Some(&hidden_seed));
|
||||
let binder_sn2 = crate::helpers::generate_nonce(&pkg.shares[1].seckey, Some(&binder_seed));
|
||||
|
||||
let member_nonces = vec![
|
||||
MemberNonce {
|
||||
idx: 1,
|
||||
hidden_pn: crate::helpers::get_pubkey(&hidden_sn1),
|
||||
binder_pn: crate::helpers::get_pubkey(&binder_sn1),
|
||||
code: [0u8; 32], // code unused in this path
|
||||
},
|
||||
MemberNonce {
|
||||
idx: 2,
|
||||
hidden_pn: crate::helpers::get_pubkey(&hidden_sn2),
|
||||
binder_pn: crate::helpers::get_pubkey(&binder_sn2),
|
||||
code: [0u8; 32],
|
||||
},
|
||||
];
|
||||
|
||||
let tweaks = vec![
|
||||
s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"),
|
||||
s32("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"),
|
||||
];
|
||||
let message = hex::decode("68656c6c6f20776f726c6421").unwrap();
|
||||
|
||||
let session = create_sign_session(
|
||||
&pkg.group,
|
||||
vec![1, 2],
|
||||
vec![(message.clone(), tweaks)],
|
||||
member_nonces,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let secret_nonce1 = SecretNoncePair {
|
||||
code: [0u8; 32],
|
||||
hidden_sn: hidden_sn1,
|
||||
binder_sn: binder_sn1,
|
||||
};
|
||||
let secret_nonce2 = SecretNoncePair {
|
||||
code: [0u8; 32],
|
||||
hidden_sn: hidden_sn2,
|
||||
binder_sn: binder_sn2,
|
||||
};
|
||||
|
||||
let psig1 = create_partial_sig_package(&session, &pkg.shares[0], &secret_nonce1).unwrap();
|
||||
let psig2 = create_partial_sig_package(&session, &pkg.shares[1], &secret_nonce2).unwrap();
|
||||
|
||||
// Verify each partial sig
|
||||
assert!(verify_partial_sig_package(&session, &pkg.group, &psig1)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
assert!(verify_partial_sig_package(&session, &pkg.group, &psig2)
|
||||
.unwrap()
|
||||
.is_none());
|
||||
|
||||
// Combine and verify final signature
|
||||
let sigs = combine_signatures(&session, &pkg.group, &[psig1, psig2]).unwrap();
|
||||
assert_eq!(sigs.len(), 1);
|
||||
assert_eq!(
|
||||
hex::encode(sigs[0].sig),
|
||||
"e76328e49c27c12392a117d39ef9f5def368590d5e72438907fb63c1006fd5891d715fa750b5840610aaf531949f633c4555ac20caf290c3f22cc0771f074447"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod ecdh_tests {
|
||||
use crate::frost::dealer::generate_dealer_package;
|
||||
use crate::frost::ecdh::*;
|
||||
use crate::frost::types::*;
|
||||
|
||||
fn s32(hex: &str) -> [u8; 32] {
|
||||
hex::decode(hex).unwrap().try_into().unwrap()
|
||||
}
|
||||
|
||||
fn p33(hex: &str) -> [u8; 33] {
|
||||
hex::decode(hex).unwrap().try_into().unwrap()
|
||||
}
|
||||
|
||||
const S0: &str = "0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f";
|
||||
const S1: &str = "0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443";
|
||||
|
||||
fn demo_keypair() -> ([u8; 32], [u8; 33]) {
|
||||
let aux = s32("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
||||
let sk = crate::helpers::generate_seckey(Some(&aux));
|
||||
let pk = crate::helpers::get_pubkey(&sk);
|
||||
(sk, pk)
|
||||
}
|
||||
|
||||
// ── create_ecdh_pkg ───────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn create_ecdh_pkg_structure() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let (_, demo_pk) = demo_keypair();
|
||||
let members = [1u32, 3u32];
|
||||
|
||||
let ecdh = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||
assert_eq!(ecdh.idx, 1);
|
||||
assert_eq!(ecdh.members, members);
|
||||
assert_eq!(ecdh.entries.len(), 1);
|
||||
assert_eq!(ecdh.entries[0].ecdh_pk, demo_pk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_ecdh_pkg_matches_fixture() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let (_, demo_pk) = demo_keypair();
|
||||
let members = [1u32, 3u32];
|
||||
|
||||
let ecdh1 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||
let ecdh3 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
hex::encode(ecdh1.entries[0].keyshare),
|
||||
"0386c5b0f4bace78ef17d02b09e339b5a39f659dbbf1f3f531b9825df6836cfea9"
|
||||
);
|
||||
assert_eq!(
|
||||
hex::encode(ecdh3.entries[0].keyshare),
|
||||
"023edaf055945d35006e1c52dd7a388e0c10b36eb55aa9d117853af87903cb54c0"
|
||||
);
|
||||
}
|
||||
|
||||
// ── create_batched_ecdh_pkg ───────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn create_batched_ecdh_pkg_multiple_keys() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let (_, demo_pk1) = demo_keypair();
|
||||
// Second key: use share[1]'s pubkey as a target
|
||||
let demo_pk2 = pkg.group.members[1].pubkey;
|
||||
let members = [1u32, 3u32];
|
||||
|
||||
let ecdh =
|
||||
create_batched_ecdh_pkg(&members, &[demo_pk1, demo_pk2], &pkg.shares[0]).unwrap();
|
||||
assert_eq!(ecdh.entries.len(), 2);
|
||||
assert_eq!(ecdh.entries[0].ecdh_pk, demo_pk1);
|
||||
assert_eq!(ecdh.entries[1].ecdh_pk, demo_pk2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_batched_ecdh_pkg_empty_keys() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let members = [1u32, 3u32];
|
||||
let ecdh = create_batched_ecdh_pkg(&members, &[], &pkg.shares[0]).unwrap();
|
||||
assert!(ecdh.entries.is_empty());
|
||||
}
|
||||
|
||||
// ── combine_ecdh_pkgs ─────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn combine_ecdh_pkgs_matches_master_secret() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let (demo_sk, demo_pk) = demo_keypair();
|
||||
let members = [1u32, 3u32];
|
||||
|
||||
let ecdh1 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||
let ecdh3 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||
|
||||
let frost_secret = combine_ecdh_pkgs(&[ecdh1, ecdh3], &demo_pk).unwrap();
|
||||
|
||||
// master_shared_secret = group_pk * demo_sk = tweak_pubkey(demo_pk, group_secret)
|
||||
let master_secret = crate::helpers::tweak_pubkey(&demo_pk, &s32(S0)).unwrap();
|
||||
assert_eq!(frost_secret, master_secret);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_ecdh_pkgs_matches_fixture() {
|
||||
let ecdh1 = EcdhPackage {
|
||||
idx: 1,
|
||||
members: vec![1, 3],
|
||||
entries: vec![EcdhEntry {
|
||||
ecdh_pk: p33("02ebd8227a6d7a03a98a1f86271d0687a6b6570187c37b39d21158a7d7835ba450"),
|
||||
keyshare: p33("0386c5b0f4bace78ef17d02b09e339b5a39f659dbbf1f3f531b9825df6836cfea9"),
|
||||
}],
|
||||
};
|
||||
let ecdh3 = EcdhPackage {
|
||||
idx: 3,
|
||||
members: vec![1, 3],
|
||||
entries: vec![EcdhEntry {
|
||||
ecdh_pk: p33("02ebd8227a6d7a03a98a1f86271d0687a6b6570187c37b39d21158a7d7835ba450"),
|
||||
keyshare: p33("023edaf055945d35006e1c52dd7a388e0c10b36eb55aa9d117853af87903cb54c0"),
|
||||
}],
|
||||
};
|
||||
let demo_pk = p33("02ebd8227a6d7a03a98a1f86271d0687a6b6570187c37b39d21158a7d7835ba450");
|
||||
let secret = combine_ecdh_pkgs(&[ecdh1, ecdh3], &demo_pk).unwrap();
|
||||
assert_eq!(
|
||||
hex::encode(secret),
|
||||
"020b6417cef5530ed4b82681945d4565ea7027f423a97b60247d07386ca3619585"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_ecdh_pkgs_missing_entry_errors() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let (_, demo_pk) = demo_keypair();
|
||||
let wrong_pk = pkg.group.members[0].pubkey; // different key
|
||||
let members = [1u32, 3u32];
|
||||
|
||||
let ecdh1 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||
let ecdh3 = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||
|
||||
// Ask for a key that's not in the packages
|
||||
let result = combine_ecdh_pkgs(&[ecdh1, ecdh3], &wrong_pk);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ── combine_batched_ecdh_pkgs ─────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn combine_batched_ecdh_pkgs_all_keys() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let (_, demo_pk1) = demo_keypair();
|
||||
let demo_pk2 = pkg.group.members[1].pubkey;
|
||||
let members = [1u32, 3u32];
|
||||
|
||||
let batch1 =
|
||||
create_batched_ecdh_pkg(&members, &[demo_pk1, demo_pk2], &pkg.shares[0]).unwrap();
|
||||
let batch3 =
|
||||
create_batched_ecdh_pkg(&members, &[demo_pk1, demo_pk2], &pkg.shares[2]).unwrap();
|
||||
|
||||
let results = combine_batched_ecdh_pkgs(&[batch1, batch3]).unwrap();
|
||||
assert_eq!(results.len(), 2);
|
||||
assert_eq!(results[0].0, demo_pk1);
|
||||
assert_eq!(results[1].0, demo_pk2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_batched_ecdh_pkgs_empty_input() {
|
||||
let results = combine_batched_ecdh_pkgs(&[]).unwrap();
|
||||
assert!(results.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combine_batched_matches_single_combine() {
|
||||
let secrets = [s32(S0), s32(S1)];
|
||||
let pkg = generate_dealer_package(2, 3, &secrets).unwrap();
|
||||
let (_, demo_pk) = demo_keypair();
|
||||
let members = [1u32, 3u32];
|
||||
|
||||
let ecdh1_single = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[0]).unwrap();
|
||||
let ecdh3_single = create_ecdh_pkg(&members, &demo_pk, &pkg.shares[2]).unwrap();
|
||||
let single_secret = combine_ecdh_pkgs(&[ecdh1_single, ecdh3_single], &demo_pk).unwrap();
|
||||
|
||||
let batch1 = create_batched_ecdh_pkg(&members, &[demo_pk], &pkg.shares[0]).unwrap();
|
||||
let batch3 = create_batched_ecdh_pkg(&members, &[demo_pk], &pkg.shares[2]).unwrap();
|
||||
let batched = combine_batched_ecdh_pkgs(&[batch1, batch3]).unwrap();
|
||||
|
||||
assert_eq!(batched[0].1, single_secret);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
/// High-level types for the FROST threshold signing protocol.
|
||||
///
|
||||
/// These mirror the bifrost TypeScript types, using raw byte arrays
|
||||
/// rather than hex strings for efficiency.
|
||||
|
||||
/// A member's secret share of the group key.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SharePackage {
|
||||
/// Participant index (1-based).
|
||||
pub idx: u32,
|
||||
/// 32-byte secret scalar.
|
||||
pub seckey: [u8; 32],
|
||||
}
|
||||
|
||||
/// A member's public identity within a group.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MemberPackage {
|
||||
/// Participant index (1-based).
|
||||
pub idx: u32,
|
||||
/// 33-byte compressed public key derived from the secret share.
|
||||
pub pubkey: [u8; 33],
|
||||
}
|
||||
|
||||
/// The group's public state: group key + member roster + threshold.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct GroupPackage {
|
||||
/// 33-byte compressed group public key.
|
||||
pub group_pk: [u8; 33],
|
||||
/// Minimum number of signers required.
|
||||
pub threshold: usize,
|
||||
/// All member public packages.
|
||||
pub members: Vec<MemberPackage>,
|
||||
}
|
||||
|
||||
/// Output of the trusted dealer: group info + all secret shares.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DealerPackage {
|
||||
pub group: GroupPackage,
|
||||
pub shares: Vec<SharePackage>,
|
||||
}
|
||||
|
||||
/// A public nonce commitment (no secret material).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublicNonce {
|
||||
/// 33-byte compressed binder nonce point.
|
||||
pub binder_pn: [u8; 33],
|
||||
/// 33-byte compressed hidden nonce point.
|
||||
pub hidden_pn: [u8; 33],
|
||||
}
|
||||
|
||||
/// A public nonce with a derivation code for secret re-derivation.
|
||||
/// Store the code; re-derive secrets on demand during signing.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DerivedNonce {
|
||||
/// 33-byte compressed binder nonce point.
|
||||
pub binder_pn: [u8; 33],
|
||||
/// 33-byte compressed hidden nonce point.
|
||||
pub hidden_pn: [u8; 33],
|
||||
/// 32-byte random derivation code.
|
||||
pub code: [u8; 32],
|
||||
}
|
||||
|
||||
/// A derived nonce tagged with the owning member's index.
|
||||
/// Used in signing wire format.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct MemberNonce {
|
||||
pub idx: u32,
|
||||
pub binder_pn: [u8; 33],
|
||||
pub hidden_pn: [u8; 33],
|
||||
pub code: [u8; 32],
|
||||
}
|
||||
|
||||
/// Secret nonce pair re-derived from a code during signing.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SecretNoncePair {
|
||||
/// The derivation code this was derived from.
|
||||
pub code: [u8; 32],
|
||||
/// 32-byte secret binder nonce scalar.
|
||||
pub binder_sn: [u8; 32],
|
||||
/// 32-byte secret hidden nonce scalar.
|
||||
pub hidden_sn: [u8; 32],
|
||||
}
|
||||
|
||||
/// A signing session: the set of messages and participating members.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SignSession {
|
||||
/// Unique session identifier (SHA-256 of session contents).
|
||||
pub sid: [u8; 32],
|
||||
/// 33-byte compressed group public key (after any tweaks are applied).
|
||||
pub group_pk: [u8; 33],
|
||||
/// Sorted list of participating member indices.
|
||||
pub members: Vec<u32>,
|
||||
/// Messages to sign. Each entry is `(message_bytes, tweaks)`.
|
||||
pub messages: Vec<(Vec<u8>, Vec<[u8; 32]>)>,
|
||||
/// Public nonces from all participating members.
|
||||
pub nonces: Vec<MemberNonce>,
|
||||
}
|
||||
|
||||
/// A partial signature produced by one member for one message.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PartialSig {
|
||||
/// The message this partial sig covers.
|
||||
pub message: Vec<u8>,
|
||||
/// 32-byte partial signature scalar.
|
||||
pub psig: [u8; 32],
|
||||
}
|
||||
|
||||
/// A partial signature package from one member covering all session messages.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PartialSigPackage {
|
||||
/// The member's index.
|
||||
pub idx: u32,
|
||||
/// The member's public key.
|
||||
pub pubkey: [u8; 33],
|
||||
/// Session ID this package belongs to.
|
||||
pub sid: [u8; 32],
|
||||
/// One partial sig per message in the session.
|
||||
pub psigs: Vec<PartialSig>,
|
||||
}
|
||||
|
||||
/// A completed signature for one message.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Signature {
|
||||
/// The message that was signed.
|
||||
pub message: Vec<u8>,
|
||||
/// 33-byte compressed group public key.
|
||||
pub pubkey: [u8; 33],
|
||||
/// 64-byte BIP340 Schnorr signature.
|
||||
pub sig: [u8; 64],
|
||||
}
|
||||
|
||||
/// One member's ECDH keyshare for a single target public key.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EcdhEntry {
|
||||
/// The target public key this share is for.
|
||||
pub ecdh_pk: [u8; 33],
|
||||
/// 33-byte compressed ECDH keyshare point.
|
||||
pub keyshare: [u8; 33],
|
||||
}
|
||||
|
||||
/// An ECDH package from one member, covering one or more target keys.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EcdhPackage {
|
||||
/// The member's index.
|
||||
pub idx: u32,
|
||||
/// The quorum members used for Lagrange interpolation.
|
||||
pub members: Vec<u32>,
|
||||
/// One entry per target public key.
|
||||
pub entries: Vec<EcdhEntry>,
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod ecc;
|
||||
pub mod frost;
|
||||
pub mod types;
|
||||
pub mod util;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user