Fix member.pubkey ambiguity between dkg and dealer

This commit is contained in:
Jon Staab
2026-02-19 18:48:12 -08:00
parent 95fd868af8
commit 079b58273f
4 changed files with 91 additions and 13 deletions
+1
View File
@@ -41,6 +41,7 @@ pub fn generate_dealer_package(
.map(|s| MemberPackage {
idx: s.idx,
pubkey: get_pubkey(&s.seckey),
identity_pk: None,
})
.collect();
+40 -8
View File
@@ -22,8 +22,8 @@
/// 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::ecc::group::{scalar_multi, serialize_element};
use crate::ecc::util::{lift_x, pow_n};
use crate::shares::{combine_set, verify_share};
use crate::types::SecretShare;
use crate::vss::{create_share_coeffs, get_share_commits, merge_share_commits};
@@ -175,15 +175,23 @@ pub fn dkg_finalize(
})?
.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).
// Build member packages.
// pubkey = aggregate share pubkey, derived from merged VSS commits:
// sum_k( merged_commits[k] * idx^k )
// This is the same equation used in share verification, but returning
// the point rather than comparing it — no secret knowledge required.
// identity_pk = each participant's first VSS commit (a_i0 * G).
let members: Vec<MemberPackage> = sorted_commits
.iter()
.map(|c| MemberPackage {
idx: c.idx,
pubkey: c.vss_commits[0],
.map(|c| {
let share_pubkey = eval_vss_pubkey(&group_vss_commits, c.idx)?;
Ok(MemberPackage {
idx: c.idx,
pubkey: share_pubkey,
identity_pk: Some(c.vss_commits[0]),
})
})
.collect();
.collect::<Result<_, Error>>()?;
let group = GroupPackage {
group_pk,
@@ -212,3 +220,27 @@ fn sum_points(points: &[[u8; 33]]) -> Result<[u8; 33], Error> {
}
Ok(serialize_element(&acc))
}
/// Evaluate the VSS commitment polynomial at a participant index to derive
/// their aggregate share public key, without knowing the secret.
///
/// Computes: sum_k( commits[k] * idx^k )
///
/// This mirrors the share verification equation in `shares::verify_share`,
/// but returns the resulting point rather than comparing it.
fn eval_vss_pubkey(commits: &[[u8; 33]], idx: u32) -> Result<[u8; 33], Error> {
if commits.is_empty() {
return Err(Error::Assertion("no VSS commits".to_string()));
}
let mut acc = None::<k256::ProjectivePoint>;
for (k, commit) in commits.iter().enumerate() {
let point = lift_x(commit)?;
let exp = pow_n(idx as u64, k as u64);
let term = scalar_multi(&point, &exp);
acc = Some(match acc {
None => term,
Some(a) => a + term,
});
}
Ok(serialize_element(&acc.unwrap()))
}
+44 -4
View File
@@ -1204,16 +1204,56 @@ mod dkg_tests {
}
#[test]
fn finalize_member_pubkeys_are_first_vss_commits() {
fn finalize_member_identity_pks_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]);
assert_eq!(group.members[0].identity_pk, Some(commit1.vss_commits[0]));
assert_eq!(group.members[1].identity_pk, Some(commit2.vss_commits[0]));
assert_eq!(group.members[2].identity_pk, Some(commit3.vss_commits[0]));
}
#[test]
fn finalize_member_pubkeys_are_share_pubkeys() {
let outputs = run_dkg();
// Each member's pubkey must equal their aggregate share seckey * G.
// We can verify this for our own slot (where we know the secret),
// and for all slots via the VSS equation.
for output in &outputs {
let expected_pk = crate::helpers::get_pubkey(&output.share.seckey);
let member = output
.group
.members
.iter()
.find(|m| m.idx == output.share.idx)
.unwrap();
assert_eq!(
member.pubkey, expected_pk,
"member {} pubkey should equal share pubkey",
output.share.idx
);
}
}
#[test]
fn finalize_dealer_members_have_no_identity_pk() {
let secrets = [
hex::decode("0070ca75929ca1ec4cd70ac34f46079bdfdd87f9d0c0bf4275f3882f7b462d0f")
.unwrap()
.try_into()
.unwrap(),
hex::decode("0e0376a7180253cdb8b6020bff0eda529760da65e715663e2152e4be2fc7b443")
.unwrap()
.try_into()
.unwrap(),
];
let pkg = crate::frost::dealer::generate_dealer_package(2, 3, &secrets).unwrap();
for member in &pkg.group.members {
assert_eq!(member.identity_pk, None);
}
}
#[test]
+6 -1
View File
@@ -17,8 +17,13 @@ pub struct SharePackage {
pub struct MemberPackage {
/// Participant index (1-based).
pub idx: u32,
/// 33-byte compressed public key derived from the secret share.
/// 33-byte compressed public key of this member's aggregate secret share (`seckey * G`).
/// Used for partial signature verification.
pub pubkey: [u8; 33],
/// DKG only: the first VSS commitment from this participant's Round 1 broadcast
/// (`a_i0 * G`), i.e. their individual identity public key.
/// `None` in the trusted dealer model.
pub identity_pk: Option<[u8; 33]>,
}
/// The group's public state: group key + member roster + threshold.