diff --git a/frost-taproot/src/frost/dkg.rs b/frost-taproot/src/frost/dkg.rs new file mode 100644 index 0000000..9fb4d57 --- /dev/null +++ b/frost-taproot/src/frost/dkg.rs @@ -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 { + 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 { + 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 { + // 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 = 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::>, |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 = 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)) +} diff --git a/frost-taproot/src/frost/mod.rs b/frost-taproot/src/frost/mod.rs index 30ebebd..9836c13 100644 --- a/frost-taproot/src/frost/mod.rs +++ b/frost-taproot/src/frost/mod.rs @@ -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; diff --git a/frost-taproot/src/frost/tests.rs b/frost-taproot/src/frost/tests.rs index 92f8707..215bece 100644 --- a/frost-taproot/src/frost/tests.rs +++ b/frost-taproot/src/frost/tests.rs @@ -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 = round1.iter().map(|(_, c)| c.clone()).collect(); + + // Round 2: each participant generates shares for all others + let all_shares: Vec = 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 = 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 = round1.iter().map(|(_, c)| c.clone()).collect(); + + // Build shares for participant 1, but tamper with one + let mut received: Vec = 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 = 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 = 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); + } +} diff --git a/frost-taproot/src/frost/types.rs b/frost-taproot/src/frost/types.rs index 507a9c2..b000da1 100644 --- a/frost-taproot/src/frost/types.rs +++ b/frost-taproot/src/frost/types.rs @@ -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 {