Add DKG
This commit is contained in:
@@ -0,0 +1,214 @@
|
||||
/// Distributed Key Generation (Pedersen DKG).
|
||||
///
|
||||
/// Eliminates the trusted dealer: each participant generates their own
|
||||
/// polynomial, broadcasts VSS commitments, and privately distributes
|
||||
/// shares. The group key is the sum of all participants' constant terms —
|
||||
/// no single party ever knows the full secret.
|
||||
///
|
||||
/// # Protocol
|
||||
///
|
||||
/// ```text
|
||||
/// Round 1 (broadcast):
|
||||
/// Each participant i calls dkg_round1() → (secret_coeffs, DkgCommitPackage)
|
||||
/// Broadcasts DkgCommitPackage to all others; keeps secret_coeffs private.
|
||||
///
|
||||
/// Round 2 (private):
|
||||
/// Each participant i calls dkg_round2() once per other participant j,
|
||||
/// producing a DkgSharePackage addressed to j. Sends it privately to j.
|
||||
/// Also calls verify_dkg_share() on each package received from others.
|
||||
///
|
||||
/// Finalize:
|
||||
/// Each participant calls dkg_finalize() with their secret coeffs,
|
||||
/// all received DkgSharePackages, and all Round 1 DkgCommitPackages.
|
||||
/// Returns DkgOutput containing their aggregate share and the GroupPackage.
|
||||
/// ```
|
||||
use crate::ecc::group::serialize_element;
|
||||
use crate::ecc::util::lift_x;
|
||||
use crate::shares::{combine_set, verify_share};
|
||||
use crate::types::SecretShare;
|
||||
use crate::vss::{create_share_coeffs, get_share_commits, merge_share_commits};
|
||||
use crate::Error;
|
||||
|
||||
use super::types::{
|
||||
DkgCommitPackage, DkgOutput, DkgSharePackage, GroupPackage, MemberPackage, SharePackage,
|
||||
};
|
||||
|
||||
/// Round 1: generate this participant's polynomial and VSS commitments.
|
||||
///
|
||||
/// Returns `(secret_coeffs, commit_package)`:
|
||||
/// - **Keep `secret_coeffs` private** — they are the raw polynomial coefficients.
|
||||
/// - **Broadcast `commit_package`** to all other participants.
|
||||
///
|
||||
/// `secrets` optionally seeds the polynomial deterministically (e.g. for
|
||||
/// testing or deterministic key derivation). Pass `&[]` for a fully random
|
||||
/// polynomial.
|
||||
pub fn dkg_round1(
|
||||
idx: u32,
|
||||
threshold: usize,
|
||||
secrets: &[[u8; 32]],
|
||||
) -> (Vec<[u8; 32]>, DkgCommitPackage) {
|
||||
let coeffs = create_share_coeffs(secrets, threshold);
|
||||
let vss_commits = get_share_commits(&coeffs);
|
||||
|
||||
// Serialize coefficients for storage/transport.
|
||||
let secret_coeffs: Vec<[u8; 32]> = coeffs.iter().map(|c| c.to_bytes().into()).collect();
|
||||
|
||||
(secret_coeffs, DkgCommitPackage { idx, vss_commits })
|
||||
}
|
||||
|
||||
/// Round 2: generate the private share for one recipient.
|
||||
///
|
||||
/// Call this once per other participant, using the `secret_coeffs` returned
|
||||
/// by [`dkg_round1`]. Send the resulting `DkgSharePackage` privately to
|
||||
/// `recipient_idx` — never broadcast it.
|
||||
pub fn dkg_round2(
|
||||
sender_idx: u32,
|
||||
secret_coeffs: &[[u8; 32]],
|
||||
recipient_idx: u32,
|
||||
) -> Result<DkgSharePackage, Error> {
|
||||
use crate::ecc::util::scalar_from_bytes;
|
||||
use crate::poly::{evaluate_x, index_to_scalar};
|
||||
|
||||
let coeffs: Vec<_> = secret_coeffs.iter().map(|c| scalar_from_bytes(c)).collect();
|
||||
let x = index_to_scalar(recipient_idx);
|
||||
let share_scalar = evaluate_x(&coeffs, x)?;
|
||||
|
||||
Ok(DkgSharePackage {
|
||||
sender_idx,
|
||||
recipient_idx,
|
||||
seckey: share_scalar.to_bytes().into(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Verify a share received during Round 2 against the sender's VSS commitments.
|
||||
///
|
||||
/// Call this for every `DkgSharePackage` you receive before accepting it.
|
||||
/// Returns `Ok(true)` if valid, `Ok(false)` if the share doesn't match the
|
||||
/// sender's commitments, or `Err` on a crypto error.
|
||||
pub fn verify_dkg_share(
|
||||
share: &DkgSharePackage,
|
||||
sender_commits: &DkgCommitPackage,
|
||||
threshold: usize,
|
||||
) -> Result<bool, Error> {
|
||||
let low_share = SecretShare {
|
||||
idx: share.recipient_idx,
|
||||
seckey: share.seckey,
|
||||
};
|
||||
verify_share(&sender_commits.vss_commits, &low_share, threshold)
|
||||
}
|
||||
|
||||
/// Finalize DKG: aggregate received shares and derive the group key.
|
||||
///
|
||||
/// - `my_idx` — this participant's own index
|
||||
/// - `my_coeffs` — the secret coefficients from your own [`dkg_round1`] call
|
||||
/// - `received` — `DkgSharePackage`s addressed to `my_idx`, one per other participant
|
||||
/// - `all_commits` — `DkgCommitPackage`s from **all** participants (including yourself)
|
||||
/// - `threshold` — the agreed signing threshold
|
||||
///
|
||||
/// Returns a [`DkgOutput`] containing this participant's aggregate share and
|
||||
/// the full [`GroupPackage`] (group public key + member roster).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - A received share fails VSS verification
|
||||
/// - The number of received shares + own share doesn't cover all participants
|
||||
/// - Any VSS commit set has the wrong length
|
||||
pub fn dkg_finalize(
|
||||
my_idx: u32,
|
||||
my_coeffs: &[[u8; 32]],
|
||||
received: &[DkgSharePackage],
|
||||
all_commits: &[DkgCommitPackage],
|
||||
threshold: usize,
|
||||
) -> Result<DkgOutput, Error> {
|
||||
// Validate all received shares against their senders' VSS commitments.
|
||||
for pkg in received {
|
||||
let sender_commits = all_commits
|
||||
.iter()
|
||||
.find(|c| c.idx == pkg.sender_idx)
|
||||
.ok_or(Error::RecordNotFound(pkg.sender_idx))?;
|
||||
if !verify_dkg_share(pkg, sender_commits, threshold)? {
|
||||
return Err(Error::Assertion(format!(
|
||||
"DKG share from participant {} failed VSS verification",
|
||||
pkg.sender_idx
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
// Compute own share: evaluate own polynomial at my_idx.
|
||||
let own_share_pkg = dkg_round2(my_idx, my_coeffs, my_idx)?;
|
||||
let own_share = SecretShare {
|
||||
idx: my_idx,
|
||||
seckey: own_share_pkg.seckey,
|
||||
};
|
||||
|
||||
// Aggregate: sum own share + all received shares at my_idx.
|
||||
let mut all_shares: Vec<SecretShare> = vec![own_share];
|
||||
for pkg in received {
|
||||
all_shares.push(SecretShare {
|
||||
idx: my_idx,
|
||||
seckey: pkg.seckey,
|
||||
});
|
||||
}
|
||||
let aggregate = combine_set(&all_shares)?;
|
||||
|
||||
// Derive the group public key: sum of all participants' first VSS commits.
|
||||
// Sort by idx for determinism.
|
||||
let mut sorted_commits = all_commits.to_vec();
|
||||
sorted_commits.sort_by_key(|c| c.idx);
|
||||
|
||||
let group_pk = {
|
||||
let first_commits: Vec<[u8; 33]> =
|
||||
sorted_commits.iter().map(|c| c.vss_commits[0]).collect();
|
||||
sum_points(&first_commits)?
|
||||
};
|
||||
|
||||
// Merge all VSS commit sets to get the group-level VSS commitments.
|
||||
// These can be used to verify any participant's aggregate share.
|
||||
let group_vss_commits = sorted_commits
|
||||
.iter()
|
||||
.try_fold(None::<Vec<[u8; 33]>>, |acc, c| {
|
||||
Ok(match acc {
|
||||
None => Some(c.vss_commits.clone()),
|
||||
Some(prev) => Some(merge_share_commits(&prev, &c.vss_commits)?),
|
||||
})
|
||||
})?
|
||||
.ok_or_else(|| Error::Assertion("no VSS commits to merge".to_string()))?;
|
||||
|
||||
// Build member packages: each member's pubkey is their first VSS commit
|
||||
// (their individual public key = a_i0 * G, the constant term of their polynomial).
|
||||
let members: Vec<MemberPackage> = sorted_commits
|
||||
.iter()
|
||||
.map(|c| MemberPackage {
|
||||
idx: c.idx,
|
||||
pubkey: c.vss_commits[0],
|
||||
})
|
||||
.collect();
|
||||
|
||||
let group = GroupPackage {
|
||||
group_pk,
|
||||
threshold,
|
||||
members,
|
||||
};
|
||||
|
||||
Ok(DkgOutput {
|
||||
share: SharePackage {
|
||||
idx: my_idx,
|
||||
seckey: aggregate.seckey,
|
||||
},
|
||||
group,
|
||||
vss_commits: group_vss_commits,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sum a list of compressed points into one point.
|
||||
fn sum_points(points: &[[u8; 33]]) -> Result<[u8; 33], Error> {
|
||||
if points.is_empty() {
|
||||
return Err(Error::Assertion("cannot sum empty point list".to_string()));
|
||||
}
|
||||
let mut acc = lift_x(&points[0])?;
|
||||
for p in &points[1..] {
|
||||
acc = acc + lift_x(p)?;
|
||||
}
|
||||
Ok(serialize_element(&acc))
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
/// // 5. Combine partial signatures into a final BIP340 signature.
|
||||
/// ```
|
||||
pub mod dealer;
|
||||
pub mod dkg;
|
||||
pub mod ecdh;
|
||||
pub mod nonce;
|
||||
pub mod signing;
|
||||
|
||||
@@ -906,3 +906,437 @@ mod ecdh_tests {
|
||||
assert_eq!(batched[0].1, single_secret);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dkg_tests {
|
||||
use crate::frost::dkg::*;
|
||||
use crate::frost::types::*;
|
||||
use crate::shares::derive_shares_secret;
|
||||
use crate::types::SecretShare;
|
||||
|
||||
fn s32(hex: &str) -> [u8; 32] {
|
||||
hex::decode(hex).unwrap().try_into().unwrap()
|
||||
}
|
||||
|
||||
fn seeds_for(seed: [&str; 2]) -> Vec<[u8; 32]> {
|
||||
seed.iter().map(|s| s32(s)).collect()
|
||||
}
|
||||
|
||||
// Deterministic seeds for 3 participants, threshold 2.
|
||||
// Two seeds per participant so both polynomial coefficients are fixed.
|
||||
// Vectors verified against the cmdruid/frost TypeScript reference.
|
||||
const SEED1: [&str; 2] = [
|
||||
"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
|
||||
"1111111111111111111111111111111111111111111111111111111111111111",
|
||||
];
|
||||
const SEED2: [&str; 2] = [
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
"2222222222222222222222222222222222222222222222222222222222222222",
|
||||
];
|
||||
const SEED3: [&str; 2] = [
|
||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
"3333333333333333333333333333333333333333333333333333333333333333",
|
||||
];
|
||||
|
||||
const GROUP_PK: &str = "033079898bb4e1f96e993da9960c411002c28e3d2c82adbda66effac39f4be050e";
|
||||
|
||||
// Expected aggregate shares per participant
|
||||
const AGG1: &str = "9999999999999999999999999999999c243bdfcc3b08592219f4dc7ff92d1715";
|
||||
const AGG2: &str = "00000000000000000000000000000003cff3694bf2261f4cc088e4598f5d3c3a";
|
||||
const AGG3: &str = "6666666666666666666666666666666a3659cfb2588c85b326ef4abff5c3a2a0";
|
||||
|
||||
// Expected group secret recoverable from any 2-of-3 aggregate shares
|
||||
const GROUP_SECRET: &str = "33333333333333333333333333333335bdd57965d4a1f2bbb38e761992c6b0af";
|
||||
|
||||
/// Run the full 3-participant DKG and return each participant's output.
|
||||
fn run_dkg() -> [DkgOutput; 3] {
|
||||
let seeds = [seeds_for(SEED1), seeds_for(SEED2), seeds_for(SEED3)];
|
||||
|
||||
// Round 1
|
||||
let round1: Vec<(Vec<[u8; 32]>, DkgCommitPackage)> = (1u32..=3)
|
||||
.zip(seeds.iter())
|
||||
.map(|(idx, s)| dkg_round1(idx, 2, s))
|
||||
.collect();
|
||||
|
||||
let all_commits: Vec<DkgCommitPackage> = round1.iter().map(|(_, c)| c.clone()).collect();
|
||||
|
||||
// Round 2: each participant generates shares for all others
|
||||
let all_shares: Vec<DkgSharePackage> = round1
|
||||
.iter()
|
||||
.flat_map(|(coeffs, commit)| {
|
||||
(1u32..=3).map(move |recipient| dkg_round2(commit.idx, coeffs, recipient).unwrap())
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Finalize each participant
|
||||
std::array::from_fn(|i| {
|
||||
let my_idx = (i + 1) as u32;
|
||||
let (my_coeffs, _) = &round1[i];
|
||||
let received: Vec<DkgSharePackage> = all_shares
|
||||
.iter()
|
||||
.filter(|s| s.recipient_idx == my_idx && s.sender_idx != my_idx)
|
||||
.cloned()
|
||||
.collect();
|
||||
dkg_finalize(my_idx, my_coeffs, &received, &all_commits, 2).unwrap()
|
||||
})
|
||||
}
|
||||
|
||||
// ── dkg_round1 ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn round1_produces_correct_commit_count() {
|
||||
let (_, commit) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
assert_eq!(commit.idx, 1);
|
||||
assert_eq!(commit.vss_commits.len(), 2); // threshold = 2
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round1_produces_correct_coeff_count() {
|
||||
let (coeffs, _) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
assert_eq!(coeffs.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round1_is_deterministic_with_seeds() {
|
||||
let (c1, pkg1) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
let (c2, pkg2) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
assert_eq!(c1, c2);
|
||||
assert_eq!(pkg1.vss_commits, pkg2.vss_commits);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round1_is_random_without_seeds() {
|
||||
let (_, pkg1) = dkg_round1(1, 2, &[]);
|
||||
let (_, pkg2) = dkg_round1(1, 2, &[]);
|
||||
assert_ne!(pkg1.vss_commits, pkg2.vss_commits);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round1_commits_match_fixture() {
|
||||
let (_, commit) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
assert_eq!(
|
||||
hex::encode(commit.vss_commits[0]),
|
||||
"026a04ab98d9e4774ad806e302dddeb63bea16b5cb5f223ee77478e861bb583eb3"
|
||||
);
|
||||
assert_eq!(
|
||||
hex::encode(commit.vss_commits[1]),
|
||||
"034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa"
|
||||
);
|
||||
}
|
||||
|
||||
// ── dkg_round2 ────────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn round2_share_matches_fixture() {
|
||||
let (coeffs, _) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
let share = dkg_round2(1, &coeffs, 2).unwrap();
|
||||
assert_eq!(share.sender_idx, 1);
|
||||
assert_eq!(share.recipient_idx, 2);
|
||||
assert_eq!(
|
||||
hex::encode(share.seckey),
|
||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn round2_all_shares_match_fixture() {
|
||||
// All 9 shares (3 senders × 3 recipients) verified against TS reference.
|
||||
let expected: &[(u32, u32, &str)] = &[
|
||||
(
|
||||
1,
|
||||
1,
|
||||
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
|
||||
),
|
||||
(
|
||||
1,
|
||||
2,
|
||||
"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc",
|
||||
),
|
||||
(
|
||||
1,
|
||||
3,
|
||||
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
),
|
||||
(
|
||||
2,
|
||||
1,
|
||||
"dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd",
|
||||
),
|
||||
(
|
||||
2,
|
||||
2,
|
||||
"000000000000000000000000000000014551231950b75fc4402da1732fc9bebe",
|
||||
),
|
||||
(
|
||||
2,
|
||||
3,
|
||||
"222222222222222222222222222222236773453b72d981e6624fc39551ebe0e0",
|
||||
),
|
||||
(
|
||||
3,
|
||||
1,
|
||||
"000000000000000000000000000000014551231950b75fc4402da1732fc9bebe",
|
||||
),
|
||||
(
|
||||
3,
|
||||
2,
|
||||
"333333333333333333333333333333347884564c83ea92f77360d4a662fcf1f1",
|
||||
),
|
||||
(
|
||||
3,
|
||||
3,
|
||||
"66666666666666666666666666666667abb7897fb71dc62aa69407d996302524",
|
||||
),
|
||||
];
|
||||
let participant_seeds = [
|
||||
(1u32, seeds_for(SEED1)),
|
||||
(2u32, seeds_for(SEED2)),
|
||||
(3u32, seeds_for(SEED3)),
|
||||
];
|
||||
for (sender_idx, seeds) in &participant_seeds {
|
||||
let (coeffs, _) = dkg_round1(*sender_idx, 2, seeds);
|
||||
for recipient_idx in 1u32..=3 {
|
||||
let share = dkg_round2(*sender_idx, &coeffs, recipient_idx).unwrap();
|
||||
let (_, _, expected_seckey) = expected
|
||||
.iter()
|
||||
.find(|(s, r, _)| *s == *sender_idx && *r == recipient_idx)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
hex::encode(share.seckey),
|
||||
*expected_seckey,
|
||||
"share {sender_idx}→{recipient_idx} mismatch"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── verify_dkg_share ──────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn verify_dkg_share_valid() {
|
||||
let (coeffs, commit) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
for recipient in 1u32..=3 {
|
||||
let share = dkg_round2(1, &coeffs, recipient).unwrap();
|
||||
assert!(
|
||||
verify_dkg_share(&share, &commit, 2).unwrap(),
|
||||
"share 1→{recipient} should verify"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_dkg_share_tampered_seckey_fails() {
|
||||
let (coeffs, commit) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
let mut share = dkg_round2(1, &coeffs, 2).unwrap();
|
||||
share.seckey[0] ^= 0xff;
|
||||
assert!(!verify_dkg_share(&share, &commit, 2).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_dkg_share_wrong_commits_fails() {
|
||||
let (coeffs1, _) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
let (_, commit2) = dkg_round1(2, 2, &seeds_for(SEED2));
|
||||
let share = dkg_round2(1, &coeffs1, 2).unwrap();
|
||||
// Verify share from participant 1 against participant 2's commits — should fail
|
||||
assert!(!verify_dkg_share(&share, &commit2, 2).unwrap());
|
||||
}
|
||||
|
||||
// ── dkg_finalize ──────────────────────────────────────────────────────────
|
||||
|
||||
#[test]
|
||||
fn finalize_group_pk_matches_fixture() {
|
||||
let outputs = run_dkg();
|
||||
for output in &outputs {
|
||||
assert_eq!(
|
||||
hex::encode(output.group.group_pk),
|
||||
GROUP_PK,
|
||||
"participant {} group_pk mismatch",
|
||||
output.share.idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_all_participants_agree_on_group_pk() {
|
||||
let outputs = run_dkg();
|
||||
let first_pk = outputs[0].group.group_pk;
|
||||
for output in &outputs[1..] {
|
||||
assert_eq!(output.group.group_pk, first_pk);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_aggregate_shares_match_fixture() {
|
||||
let outputs = run_dkg();
|
||||
assert_eq!(hex::encode(outputs[0].share.seckey), AGG1);
|
||||
assert_eq!(hex::encode(outputs[1].share.seckey), AGG2);
|
||||
assert_eq!(hex::encode(outputs[2].share.seckey), AGG3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_threshold_subsets_recover_secret() {
|
||||
let outputs = run_dkg();
|
||||
let to_low = |o: &DkgOutput| SecretShare {
|
||||
idx: o.share.idx,
|
||||
seckey: o.share.seckey,
|
||||
};
|
||||
|
||||
// All three 2-of-3 subsets should recover the same secret
|
||||
let s12 = derive_shares_secret(&[to_low(&outputs[0]), to_low(&outputs[1])]).unwrap();
|
||||
let s13 = derive_shares_secret(&[to_low(&outputs[0]), to_low(&outputs[2])]).unwrap();
|
||||
let s23 = derive_shares_secret(&[to_low(&outputs[1]), to_low(&outputs[2])]).unwrap();
|
||||
|
||||
assert_eq!(hex::encode(s12), GROUP_SECRET);
|
||||
assert_eq!(hex::encode(s13), GROUP_SECRET);
|
||||
assert_eq!(hex::encode(s23), GROUP_SECRET);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_recovered_secret_matches_group_pk() {
|
||||
let outputs = run_dkg();
|
||||
let to_low = |o: &DkgOutput| SecretShare {
|
||||
idx: o.share.idx,
|
||||
seckey: o.share.seckey,
|
||||
};
|
||||
let secret = derive_shares_secret(&[to_low(&outputs[0]), to_low(&outputs[1])]).unwrap();
|
||||
let pk = crate::helpers::get_pubkey(&secret);
|
||||
assert_eq!(pk, outputs[0].group.group_pk);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_member_pubkeys_are_first_vss_commits() {
|
||||
let outputs = run_dkg();
|
||||
let (_, commit1) = dkg_round1(1, 2, &seeds_for(SEED1));
|
||||
let (_, commit2) = dkg_round1(2, 2, &seeds_for(SEED2));
|
||||
let (_, commit3) = dkg_round1(3, 2, &seeds_for(SEED3));
|
||||
|
||||
let group = &outputs[0].group;
|
||||
assert_eq!(group.members[0].pubkey, commit1.vss_commits[0]);
|
||||
assert_eq!(group.members[1].pubkey, commit2.vss_commits[0]);
|
||||
assert_eq!(group.members[2].pubkey, commit3.vss_commits[0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_threshold_is_set_correctly() {
|
||||
let outputs = run_dkg();
|
||||
for output in &outputs {
|
||||
assert_eq!(output.group.threshold, 2);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_vss_commits_verify_aggregate_shares() {
|
||||
let outputs = run_dkg();
|
||||
for output in &outputs {
|
||||
let low_share = SecretShare {
|
||||
idx: output.share.idx,
|
||||
seckey: output.share.seckey,
|
||||
};
|
||||
let valid = crate::shares::verify_share(
|
||||
&output.vss_commits,
|
||||
&low_share,
|
||||
output.group.threshold,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(
|
||||
valid,
|
||||
"aggregate share for participant {} failed VSS verification",
|
||||
output.share.idx
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_rejects_tampered_share() {
|
||||
let seeds = [seeds_for(SEED1), seeds_for(SEED2), seeds_for(SEED3)];
|
||||
let round1: Vec<(Vec<[u8; 32]>, DkgCommitPackage)> = (1u32..=3)
|
||||
.zip(seeds.iter())
|
||||
.map(|(idx, s)| dkg_round1(idx, 2, s))
|
||||
.collect();
|
||||
let all_commits: Vec<DkgCommitPackage> = round1.iter().map(|(_, c)| c.clone()).collect();
|
||||
|
||||
// Build shares for participant 1, but tamper with one
|
||||
let mut received: Vec<DkgSharePackage> = round1
|
||||
.iter()
|
||||
.filter(|(_, c)| c.idx != 1)
|
||||
.map(|(coeffs, commit)| dkg_round2(commit.idx, coeffs, 1).unwrap())
|
||||
.collect();
|
||||
received[0].seckey[0] ^= 0xff; // tamper
|
||||
|
||||
let result = dkg_finalize(1, &round1[0].0, &received, &all_commits, 2);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn finalize_rejects_unknown_sender() {
|
||||
let seeds = [seeds_for(SEED1), seeds_for(SEED2)];
|
||||
let round1: Vec<(Vec<[u8; 32]>, DkgCommitPackage)> = (1u32..=2)
|
||||
.zip(seeds.iter())
|
||||
.map(|(idx, s)| dkg_round1(idx, 2, s))
|
||||
.collect();
|
||||
let all_commits: Vec<DkgCommitPackage> = round1.iter().map(|(_, c)| c.clone()).collect();
|
||||
|
||||
// A share from a sender (idx=99) not in all_commits
|
||||
let ghost_share = DkgSharePackage {
|
||||
sender_idx: 99,
|
||||
recipient_idx: 1,
|
||||
seckey: [0u8; 32],
|
||||
};
|
||||
|
||||
let result = dkg_finalize(1, &round1[0].0, &[ghost_share], &all_commits, 2);
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
// ── end-to-end: DKG output is usable for signing ──────────────────────────
|
||||
|
||||
#[test]
|
||||
fn dkg_output_can_sign() {
|
||||
use crate::frost::nonce::{derive_secret_nonce, generate_nonce_pair, to_member_nonce};
|
||||
use crate::frost::signing::{
|
||||
combine_signatures, create_partial_sig_package, create_sign_session,
|
||||
};
|
||||
use crate::frost::types::MemberNonce;
|
||||
|
||||
let outputs = run_dkg();
|
||||
|
||||
// Use participants 1 and 2 to sign
|
||||
let signers = &outputs[..2];
|
||||
let message = b"hello from dkg";
|
||||
|
||||
let nonce_pairs: Vec<_> = signers
|
||||
.iter()
|
||||
.map(|o| generate_nonce_pair(&o.share.seckey))
|
||||
.collect();
|
||||
|
||||
let member_nonces: Vec<MemberNonce> = signers
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(o, n)| to_member_nonce(n.clone(), o.share.idx))
|
||||
.collect();
|
||||
|
||||
let secret_nonces: Vec<_> = signers
|
||||
.iter()
|
||||
.zip(nonce_pairs.iter())
|
||||
.map(|(o, n)| derive_secret_nonce(&o.share.seckey, &n.code))
|
||||
.collect();
|
||||
|
||||
let session = create_sign_session(
|
||||
&outputs[0].group,
|
||||
signers.iter().map(|o| o.share.idx).collect(),
|
||||
vec![(message.to_vec(), vec![])],
|
||||
member_nonces,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let psig1 =
|
||||
create_partial_sig_package(&session, &signers[0].share, &secret_nonces[0]).unwrap();
|
||||
let psig2 =
|
||||
create_partial_sig_package(&session, &signers[1].share, &secret_nonces[1]).unwrap();
|
||||
|
||||
let sigs = combine_signatures(&session, &outputs[0].group, &[psig1, psig2]).unwrap();
|
||||
assert_eq!(sigs.len(), 1);
|
||||
assert_eq!(sigs[0].message, message);
|
||||
// Signature is 64 bytes and was verified internally by combine_signatures
|
||||
assert_eq!(sigs[0].sig.len(), 64);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -129,6 +129,40 @@ pub struct Signature {
|
||||
pub sig: [u8; 64],
|
||||
}
|
||||
|
||||
/// One participant's Round 1 broadcast: their VSS commitments.
|
||||
/// Keep the corresponding secret coefficients private; broadcast this.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct DkgCommitPackage {
|
||||
/// This participant's index (1-based).
|
||||
pub idx: u32,
|
||||
/// VSS commitments: one compressed point per polynomial coefficient.
|
||||
pub vss_commits: Vec<[u8; 33]>,
|
||||
}
|
||||
|
||||
/// One participant's Round 2 private message to a specific recipient.
|
||||
/// Send this only to the participant identified by `recipient_idx`.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DkgSharePackage {
|
||||
/// Index of the participant who generated this share.
|
||||
pub sender_idx: u32,
|
||||
/// Index of the intended recipient.
|
||||
pub recipient_idx: u32,
|
||||
/// 32-byte secret share scalar (private — send only to recipient).
|
||||
pub seckey: [u8; 32],
|
||||
}
|
||||
|
||||
/// A participant's complete local state after DKG finalization.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DkgOutput {
|
||||
/// This participant's aggregate secret share of the group key.
|
||||
pub share: SharePackage,
|
||||
/// The group's public state, usable for signing.
|
||||
pub group: GroupPackage,
|
||||
/// Merged VSS commitments for the whole group.
|
||||
/// Use these to verify any participant's aggregate share via `verify_share`.
|
||||
pub vss_commits: Vec<[u8; 33]>,
|
||||
}
|
||||
|
||||
/// One member's ECDH keyshare for a single target public key.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct EcdhEntry {
|
||||
|
||||
Reference in New Issue
Block a user