This commit is contained in:
Jon Staab
2026-02-19 18:39:33 -08:00
parent 327809e837
commit 95fd868af8
4 changed files with 683 additions and 0 deletions
+214
View File
@@ -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))
}
+1
View File
@@ -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;
+434
View File
@@ -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);
}
}
+34
View File
@@ -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 {