Add high level api

This commit is contained in:
Jon Staab
2026-02-19 18:29:30 -08:00
parent 336a151198
commit 327809e837
10 changed files with 1642 additions and 0 deletions
+1
View File
@@ -136,6 +136,7 @@ name = "frost-taproot"
version = "0.1.0"
dependencies = [
"hex",
"hmac",
"k256",
"rand",
"sha2",
+1
View File
@@ -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"
+109
View File
@@ -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)
}
+113
View File
@@ -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()
}
+33
View File
@@ -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::*;
+95
View File
@@ -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()
}
+231
View File
@@ -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()
}
+908
View File
@@ -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);
}
}
+150
View File
@@ -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
View File
@@ -1,4 +1,5 @@
pub mod ecc;
pub mod frost;
pub mod types;
pub mod util;