From f5aa96527be45a134f91f0117b476584719a161d Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:33:33 -0300 Subject: [PATCH 01/23] Move code as-is from private repo --- crates/frost-bls12-381-g1/Cargo.toml | 29 + crates/frost-bls12-381-g1/README.md | 7 + crates/frost-bls12-381-g1/dkg.md | 72 +++ crates/frost-bls12-381-g1/src/curve.rs | 267 ++++++++ crates/frost-bls12-381-g1/src/frost_core.rs | 474 ++++++++++++++ crates/frost-bls12-381-g1/src/kryptology.rs | 601 ++++++++++++++++++ crates/frost-bls12-381-g1/src/lib.rs | 19 + crates/frost-bls12-381-g1/src/tests.rs | 300 +++++++++ .../kryptology_fixtures/2-of-3-ctx-0.json | 98 +++ .../kryptology_fixtures/3-of-3-ctx-0.json | 101 +++ .../kryptology_fixtures/invalid-proof.json | 94 +++ .../malformed-share-id.json | 96 +++ .../tests/kryptology_interop.rs | 262 ++++++++ .../tests/kryptology_round_trip.rs | 111 ++++ 14 files changed, 2531 insertions(+) create mode 100644 crates/frost-bls12-381-g1/Cargo.toml create mode 100644 crates/frost-bls12-381-g1/README.md create mode 100644 crates/frost-bls12-381-g1/dkg.md create mode 100644 crates/frost-bls12-381-g1/src/curve.rs create mode 100644 crates/frost-bls12-381-g1/src/frost_core.rs create mode 100644 crates/frost-bls12-381-g1/src/kryptology.rs create mode 100644 crates/frost-bls12-381-g1/src/lib.rs create mode 100644 crates/frost-bls12-381-g1/src/tests.rs create mode 100644 crates/frost-bls12-381-g1/tests/kryptology_fixtures/2-of-3-ctx-0.json create mode 100644 crates/frost-bls12-381-g1/tests/kryptology_fixtures/3-of-3-ctx-0.json create mode 100644 crates/frost-bls12-381-g1/tests/kryptology_fixtures/invalid-proof.json create mode 100644 crates/frost-bls12-381-g1/tests/kryptology_fixtures/malformed-share-id.json create mode 100644 crates/frost-bls12-381-g1/tests/kryptology_interop.rs create mode 100644 crates/frost-bls12-381-g1/tests/kryptology_round_trip.rs diff --git a/crates/frost-bls12-381-g1/Cargo.toml b/crates/frost-bls12-381-g1/Cargo.toml new file mode 100644 index 00000000..8ca8c1dd --- /dev/null +++ b/crates/frost-bls12-381-g1/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "frost-bls12-381-g1" +edition.workspace = true +rust-version.workspace = true +version.workspace = true +authors.workspace = true +readme = "README.md" +license.workspace = true +repository.workspace = true +categories.workspace = true +keywords = ["cryptography", "crypto", "bls12-381", "threshold", "signature"] +description = "Kryptology-compatible FROST DKG and BLS threshold signing over BLS12-381 G1." + +[dependencies] +blst = { version = "0.3", default-features = false } +rand_core.workspace = true +sha2 = { version = "0.10.2", default-features = false } + +[dev-dependencies] +hex.workspace = true +lazy_static.workspace = true +rand.workspace = true +rand_chacha.workspace = true +serde_json.workspace = true + +[lib] +# Disables non-criterion benchmark which is not used; prevents errors +# when using criterion-specific flags +bench = false diff --git a/crates/frost-bls12-381-g1/README.md b/crates/frost-bls12-381-g1/README.md new file mode 100644 index 00000000..0c9f38fd --- /dev/null +++ b/crates/frost-bls12-381-g1/README.md @@ -0,0 +1,7 @@ +# frost-bls12-381-g1 + +Kryptology-compatible FROST DKG and BLS threshold signing over BLS12-381 G1. + +This crate implements a distributed key generation protocol compatible with +Go's Coinbase Kryptology FROST DKG, and BLS threshold signing (Ethereum 2.0 +compatible). diff --git a/crates/frost-bls12-381-g1/dkg.md b/crates/frost-bls12-381-g1/dkg.md new file mode 100644 index 00000000..0e0c3ba4 --- /dev/null +++ b/crates/frost-bls12-381-g1/dkg.md @@ -0,0 +1,72 @@ +# Kryptology-Compatible Distributed Key Generation (DKG) + +The kryptology DKG module supports generating FROST key shares in a distributed +manner compatible with Go's Coinbase Kryptology FROST DKG. + +The output types ([`KeyPackage`], [`PublicKeyPackage`]) are standard frost-core +types. The key shares can be used for BLS threshold signing via the +`bls_partial_sign`, `bls_combine_signatures`, and `bls_verify` functions. + +## Wire contract + +The supported cross-language contract is the raw field encoding used by the +fixtures and round helpers in this module: + +- Scalars are 32-byte big-endian field elements. +- G1 points are 48-byte compressed encodings. +- Participant identifiers are transported as `u32` values. +- The DKG context is transported as a single `u8` byte. + +Gob encoding is not part of this interoperability contract. + +## Example + +```rust +use std::collections::BTreeMap; + +use frost_bls12_381_g1::kryptology; + +let mut rng = rand::rngs::OsRng; + +let threshold = 3u16; +let max_signers = 5u16; +let ctx = 0u8; + +// Round 1: each participant generates broadcast data and shares. +let mut bcasts = BTreeMap::new(); +let mut all_shares: BTreeMap> = BTreeMap::new(); +let mut secrets = BTreeMap::new(); + +for id in 1..=max_signers as u32 { + let (bcast, shares, secret) = + kryptology::round1(id, threshold, max_signers, ctx, &mut rng) + .expect("round1 should succeed"); + bcasts.insert(id, bcast); + secrets.insert(id, secret); + for (&target_id, share) in &shares { + all_shares.entry(target_id).or_default().insert(id, share.clone()); + } +} + +// Round 2: each participant verifies broadcasts and aggregates shares. +let mut key_packages = BTreeMap::new(); +let mut public_key_packages = Vec::new(); + +for id in 1..=max_signers as u32 { + let received_bcasts: BTreeMap<_, _> = bcasts + .iter() + .filter(|(&k, _)| k != id) + .map(|(&k, v)| (k, v.clone())) + .collect(); + let received_shares = all_shares.remove(&id).unwrap(); + let secret = secrets.remove(&id).unwrap(); + + let (_r2_bcast, key_package, pub_package) = + kryptology::round2(secret, &received_bcasts, &received_shares) + .expect("round2 should succeed"); + key_packages.insert(id, key_package); + public_key_packages.push(pub_package); +} + +// Each participant now has a KeyPackage and PublicKeyPackage for BLS threshold signing. +``` diff --git a/crates/frost-bls12-381-g1/src/curve.rs b/crates/frost-bls12-381-g1/src/curve.rs new file mode 100644 index 00000000..088caf41 --- /dev/null +++ b/crates/frost-bls12-381-g1/src/curve.rs @@ -0,0 +1,267 @@ +//! Thin wrappers around [`blst`] types for the BLS12-381 scalar field and G1 curve group. +//! +//! Provides [`Scalar`], [`G1Projective`], and [`G1Affine`] with arithmetic operator +//! overloads, serialization, and safe constructors that enforce subgroup membership. + +use core::fmt; +use core::ops::{Add, Mul, Sub}; + +use blst::*; +use rand_core::{CryptoRng, RngCore}; + +/// BLS12-381 scalar field element. Wrapper around `blst_fr` in Montgomery form. +#[derive(Copy, Clone, Default, PartialEq, Eq)] +pub struct Scalar(pub(crate) blst_fr); + +impl fmt::Debug for Scalar { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("Scalar").field(&self.to_bytes()).finish() + } +} + +impl Scalar { + /// Additive identity. + pub const ZERO: Self = Scalar(blst_fr { l: [0; 4] }); + + /// Multiplicative identity. + pub const ONE: Self = { + // Montgomery form of 1 for BLS12-381 scalar field. + // R mod r where R = 2^256 and r is the scalar field order. + // Computed from: blst_scalar_from_uint64([1,0,0,0]) -> blst_fr_from_scalar + // Pre-computed constant: + Scalar(blst_fr { + l: [ + 0x0000_0001_ffff_fffe, + 0x5884_b7fa_0003_4802, + 0x998c_4fef_ecbc_4ff5, + 0x1824_b159_acc5_056f, + ], + }) + }; + + /// Serialize to 32 little-endian bytes. + pub fn to_bytes(&self) -> [u8; 32] { + let mut scalar = blst_scalar::default(); + let mut out = [0u8; 32]; + unsafe { + blst_scalar_from_fr(&mut scalar, &self.0); + blst_lendian_from_scalar(out.as_mut_ptr(), &scalar); + } + out + } + + /// Deserialize from 32 little-endian bytes. Returns `None` if invalid. + pub fn from_bytes(bytes: &[u8; 32]) -> Option { + let mut scalar = blst_scalar::default(); + unsafe { + blst_scalar_from_lendian(&mut scalar, bytes.as_ptr()); + if !blst_scalar_fr_check(&scalar) { + return None; + } + let mut fr = blst_fr::default(); + blst_fr_from_scalar(&mut fr, &scalar); + Some(Scalar(fr)) + } + } + + /// Reduce 64 little-endian bytes modulo the scalar field order. + pub fn from_bytes_wide(bytes: &[u8; 64]) -> Self { + let mut scalar = blst_scalar::default(); + let mut fr = blst_fr::default(); + unsafe { + blst_scalar_from_le_bytes(&mut scalar, bytes.as_ptr(), 64); + blst_fr_from_scalar(&mut fr, &scalar); + } + Scalar(fr) + } + + /// Generate a uniformly random scalar. + pub fn random(rng: &mut R) -> Self { + let mut wide = [0u8; 64]; + rng.fill_bytes(&mut wide); + Self::from_bytes_wide(&wide) + } + + /// Compute the multiplicative inverse. Returns `None` for zero. + pub fn invert(&self) -> Option { + if *self == Self::ZERO { + return None; + } + let mut out = blst_fr::default(); + unsafe { blst_fr_eucl_inverse(&mut out, &self.0) }; + Some(Scalar(out)) + } +} + +impl From for Scalar { + fn from(val: u64) -> Self { + let mut fr = blst_fr::default(); + let limbs: [u64; 4] = [val, 0, 0, 0]; + unsafe { blst_fr_from_uint64(&mut fr, limbs.as_ptr()) }; + Scalar(fr) + } +} + +impl Add for Scalar { + type Output = Self; + fn add(self, rhs: Self) -> Self { + let mut out = blst_fr::default(); + unsafe { blst_fr_add(&mut out, &self.0, &rhs.0) }; + Scalar(out) + } +} + +impl Sub for Scalar { + type Output = Self; + fn sub(self, rhs: Self) -> Self { + let mut out = blst_fr::default(); + unsafe { blst_fr_sub(&mut out, &self.0, &rhs.0) }; + Scalar(out) + } +} + +impl Mul for Scalar { + type Output = Self; + fn mul(self, rhs: Self) -> Self { + let mut out = blst_fr::default(); + unsafe { blst_fr_mul(&mut out, &self.0, &rhs.0) }; + Scalar(out) + } +} + +/// BLS12-381 G1 point in projective (Jacobian) coordinates. Wrapper around `blst_p1`. +#[derive(Copy, Clone, Default, Eq)] +pub struct G1Projective(pub(crate) blst_p1); + +impl PartialEq for G1Projective { + fn eq(&self, other: &Self) -> bool { + unsafe { blst_p1_is_equal(&self.0, &other.0) } + } +} + +impl fmt::Debug for G1Projective { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("G1Projective") + .field(&G1Affine::from(*self).to_compressed()) + .finish() + } +} + +impl G1Projective { + /// The fixed generator of G1. + pub fn generator() -> Self { + unsafe { G1Projective(*blst_p1_generator()) } + } + + /// The identity (point at infinity). + pub fn identity() -> Self { + Self::default() + } + + /// Check whether this is the identity element. + pub fn is_identity(&self) -> bool { + unsafe { blst_p1_is_inf(&self.0) } + } + + /// Deserialize from 48-byte compressed form. + /// Returns `None` on invalid encoding or point not in G1, or the identity (point at infinity). + pub fn from_compressed(bytes: &[u8; 48]) -> Option { + let affine = G1Affine::from_compressed(bytes)?; + if affine.is_identity() { + return None; + } + Some(G1Projective::from(affine)) + } +} + +impl Add for G1Projective { + type Output = Self; + fn add(self, rhs: Self) -> Self { + let mut out = blst_p1::default(); + unsafe { blst_p1_add_or_double(&mut out, &self.0, &rhs.0) }; + G1Projective(out) + } +} + +impl Sub for G1Projective { + type Output = Self; + fn sub(self, rhs: Self) -> Self { + let mut neg = rhs.0; + let mut out = blst_p1::default(); + unsafe { + blst_p1_cneg(&mut neg, true); + blst_p1_add_or_double(&mut out, &self.0, &neg); + } + G1Projective(out) + } +} + +impl Mul for G1Projective { + type Output = Self; + fn mul(self, rhs: Scalar) -> Self { + let mut scalar = blst_scalar::default(); + let mut out = blst_p1::default(); + unsafe { + blst_scalar_from_fr(&mut scalar, &rhs.0); + blst_p1_mult(&mut out, &self.0, scalar.b.as_ptr(), 255); + } + G1Projective(out) + } +} + +/// BLS12-381 G1 point in affine coordinates (for serialization). Wrapper around `blst_p1_affine`. +#[derive(Copy, Clone, Default)] +pub struct G1Affine(pub(crate) blst_p1_affine); + +impl G1Affine { + /// Serialize to 48-byte compressed form. + pub fn to_compressed(&self) -> [u8; 48] { + unsafe { + let mut out = [0u8; 48]; + blst_p1_affine_compress(out.as_mut_ptr(), &self.0); + out + } + } + + /// Deserialize from 48-byte compressed form. + /// Returns `None` on invalid encoding or point not in G1. + pub fn from_compressed(bytes: &[u8; 48]) -> Option { + let mut affine = blst_p1_affine::default(); + unsafe { + if blst_p1_uncompress(&mut affine, bytes.as_ptr()) != BLST_ERROR::BLST_SUCCESS { + return None; + } + if !blst_p1_affine_in_g1(&affine) { + return None; + } + } + Some(G1Affine(affine)) + } + + /// Check whether this is the identity (point at infinity). + pub fn is_identity(&self) -> bool { + unsafe { blst_p1_affine_is_inf(&self.0) } + } +} + +impl From for G1Affine { + fn from(p: G1Projective) -> Self { + let mut affine = blst_p1_affine::default(); + unsafe { blst_p1_to_affine(&mut affine, &p.0) }; + G1Affine(affine) + } +} + +impl From<&G1Projective> for G1Affine { + fn from(p: &G1Projective) -> Self { + G1Affine::from(*p) + } +} + +impl From for G1Projective { + fn from(a: G1Affine) -> Self { + let mut p = blst_p1::default(); + unsafe { blst_p1_from_affine(&mut p, &a.0) }; + G1Projective(p) + } +} diff --git a/crates/frost-bls12-381-g1/src/frost_core.rs b/crates/frost-bls12-381-g1/src/frost_core.rs new file mode 100644 index 00000000..c15dbcf0 --- /dev/null +++ b/crates/frost-bls12-381-g1/src/frost_core.rs @@ -0,0 +1,474 @@ +//! Port of frost-core types and functions, specialized for BLS12-381 G1 curve operations. +//! +//! Contains the key material types (identifiers, shares, packages) and the +//! polynomial evaluation functions needed by the kryptology-compatible DKG. + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::vec; +use alloc::vec::Vec; +use core::cmp::Ordering; + +use super::*; + +/// Errors from key operations. +#[derive(Debug)] +pub enum FrostCoreError { + /// Participant ID is zero. + InvalidZeroScalar, + /// Invalid number of minimum signers (must be >= 2 and <= max_signers). + InvalidMinSigners, + /// Invalid number of maximum signers (must be >= 2). + InvalidMaxSigners, + /// The secret share verification (Feldman VSS) failed. + InvalidSecretShare, + /// Commitment count mismatch during aggregation. + IncorrectNumberOfCommitments, + /// The commitment has no coefficients. + IncorrectCommitment, +} + +/// A participant identifier wrapping a non-zero scalar. +/// +/// Ported from frost-core/src/identifier.rs:26-48 +#[derive(Copy, Clone, Debug)] +pub struct Identifier(Scalar); + +impl Identifier { + /// Create a new identifier from a non-zero u32. + pub fn from_u32(id: u32) -> Result { + let scalar = Scalar::from(id as u64); + if scalar == Scalar::ZERO { + Err(FrostCoreError::InvalidZeroScalar) + } else { + Ok(Self(scalar)) + } + } + + /// Return the underlying scalar. + pub fn to_scalar(&self) -> Scalar { + self.0 + } +} + +impl PartialEq for Identifier { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for Identifier {} + +impl PartialOrd for Identifier { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Identifier { + fn cmp(&self, other: &Self) -> Ordering { + // Compare using serialized bytes in little-endian order. + // Ported from frost-core/src/identifier.rs:131-146 + let a = self.0.to_bytes(); + let b = other.0.to_bytes(); + // Compare most-significant byte first (reversed from LE storage). + for i in (0..32).rev() { + match a[i].cmp(&b[i]) { + Ordering::Equal => continue, + other => return other, + } + } + Ordering::Equal + } +} + +/// A commitment to a single polynomial coefficient (a group element). +/// +/// Ported from frost-core/src/keys.rs:249-274 +#[derive(Copy, Clone, Debug)] +pub struct CoefficientCommitment(G1Projective); + +impl CoefficientCommitment { + /// Create a new coefficient commitment. + pub fn new(value: G1Projective) -> Self { + Self(value) + } + + /// Return the underlying group element. + pub fn value(&self) -> G1Projective { + self.0 + } +} + +/// The commitments to the coefficients of a secret polynomial, used for +/// Feldman verifiable secret sharing. +/// +/// Ported from frost-core/src/keys.rs:308-382 +#[derive(Clone, Debug)] +pub struct VerifiableSecretSharingCommitment(Vec); + +impl VerifiableSecretSharingCommitment { + /// Create from a vector of coefficient commitments. + pub fn new(coefficients: Vec) -> Self { + Self(coefficients) + } + + /// Return the coefficient commitments. + pub fn coefficients(&self) -> &[CoefficientCommitment] { + &self.0 + } + + /// Derive a VSS commitment from a list of compressed group elements. + pub fn from_commitments(commitments: &[[u8; 48]]) -> Option { + let cc = commitments + .iter() + .map(|bytes| G1Projective::from_compressed(bytes).map(CoefficientCommitment::new)) + .collect::>>()?; + + Some(VerifiableSecretSharingCommitment::new(cc)) + } +} + +/// A secret scalar value representing a signer's share of the group secret. +/// +/// Ported from frost-core/src/keys.rs:87-121 +#[derive(Copy, Clone, Debug)] +pub struct SigningShare(Scalar); + +impl SigningShare { + /// Create a signing share from a scalar. + /// + /// Ported from frost-core/src/keys.rs:96-98 + pub fn new(scalar: Scalar) -> Self { + Self(scalar) + } + + /// Return the underlying scalar. + /// + /// Ported from frost-core/src/keys.rs:103-105 + pub fn to_scalar(&self) -> Scalar { + self.0 + } + + /// Evaluate the polynomial defined by `coefficients` at `peer`. + /// + /// Ported from frost-core/src/keys.rs:119-121 + pub fn from_coefficients(coefficients: &[Scalar], peer: Identifier) -> Self { + Self::new(evaluate_polynomial(peer, coefficients)) + } +} +/// A public group element that represents a single signer's public +/// verification share. +/// +/// Ported from frost-core/src/keys.rs:163-214 +#[derive(Copy, Clone, Debug)] +pub struct VerifyingShare(G1Projective); + +impl VerifyingShare { + /// Create a verifying share from a group element. + pub fn new(element: G1Projective) -> Self { + Self(element) + } + + /// Return the underlying group element. + pub fn to_element(&self) -> G1Projective { + self.0 + } + + /// Compute the verifying share for `identifier` from the summed VSS + /// commitment. + /// + /// Ported from frost-core/src/keys.rs:198-214 + pub fn from_commitment( + identifier: Identifier, + commitment: &VerifiableSecretSharingCommitment, + ) -> Self { + Self::new(evaluate_vss(identifier, commitment)) + } +} + +/// The group public key, used to verify threshold signatures. +/// +/// Ported from frost-core/src/verifying_key.rs:15-93 +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct VerifyingKey(G1Projective); + +impl VerifyingKey { + /// Create a verifying key from a group element. + pub fn new(element: G1Projective) -> Self { + Self(element) + } + + /// Return the underlying group element. + pub fn to_element(&self) -> G1Projective { + self.0 + } + + /// Derive the verifying key from the first coefficient commitment. + /// + /// Ported from frost-core/src/verifying_key.rs:83-93 + pub fn from_commitment( + commitment: &VerifiableSecretSharingCommitment, + ) -> Result { + Ok(Self::new( + commitment + .coefficients() + .first() + .ok_or(FrostCoreError::IncorrectCommitment)? + .value(), + )) + } +} + +/// Secret and public key material generated during DKG. +/// +/// Ported from frost-core/src/keys.rs:399-468 +pub struct SecretShare { + identifier: Identifier, + signing_share: SigningShare, + commitment: VerifiableSecretSharingCommitment, +} + +impl SecretShare { + /// Create a new secret share. + /// + /// Ported from frost-core/src/keys.rs:418-429 + pub fn new( + identifier: Identifier, + signing_share: SigningShare, + commitment: VerifiableSecretSharingCommitment, + ) -> Self { + Self { + identifier, + signing_share, + commitment, + } + } + + /// Verify the share against the commitment using Feldman VSS. + /// + /// Checks that `G * signing_share == evaluate_vss(identifier, commitment)`. + /// + /// Ported from frost-core/src/keys.rs:445-468 + pub fn verify(&self) -> Result<(), FrostCoreError> { + let f_result = G1Projective::generator() * self.signing_share.to_scalar(); + let result = evaluate_vss(self.identifier, &self.commitment); + + if f_result != result { + return Err(FrostCoreError::InvalidSecretShare); + } + + Ok(()) + } +} + +/// A key package containing all key material for a participant. +/// +/// Ported from frost-core/src/keys.rs:627-665 +#[derive(Debug)] +pub struct KeyPackage { + identifier: Identifier, + signing_share: SigningShare, + verifying_share: VerifyingShare, + verifying_key: VerifyingKey, + min_signers: u16, +} + +impl KeyPackage { + /// Create a new key package. + /// + /// Ported from frost-core/src/keys.rs:650-665 + pub fn new( + identifier: Identifier, + signing_share: SigningShare, + verifying_share: VerifyingShare, + verifying_key: VerifyingKey, + min_signers: u16, + ) -> Self { + Self { + identifier, + signing_share, + verifying_share, + verifying_key, + min_signers, + } + } + + /// The participant identifier. + pub fn identifier(&self) -> &Identifier { + &self.identifier + } + + /// The signing share (secret). + pub fn signing_share(&self) -> &SigningShare { + &self.signing_share + } + + /// The participant's public verifying share. + pub fn verifying_share(&self) -> &VerifyingShare { + &self.verifying_share + } + + /// The group public key. + pub fn verifying_key(&self) -> &VerifyingKey { + &self.verifying_key + } + + /// The minimum number of signers. + pub fn min_signers(&self) -> u16 { + self.min_signers + } +} + +/// Public data containing all signers' verification shares and the group +/// public key. +/// +/// Ported from frost-core/src/keys.rs:720-777 +#[derive(Debug)] +pub struct PublicKeyPackage { + verifying_shares: BTreeMap, + verifying_key: VerifyingKey, +} + +impl PublicKeyPackage { + /// Create a new public key package. + /// + /// Ported from frost-core/src/keys.rs:736-745 + pub fn new( + verifying_shares: BTreeMap, + verifying_key: VerifyingKey, + ) -> Self { + Self { + verifying_shares, + verifying_key, + } + } + + /// The group public key. + pub fn verifying_key(&self) -> &VerifyingKey { + &self.verifying_key + } + + /// The verifying shares for all participants. + pub fn verifying_shares(&self) -> &BTreeMap { + &self.verifying_shares + } + + /// Derive a public key package from all participants' DKG commitments. + /// + /// Ported from frost-core/src/keys.rs:770-777 + pub fn from_dkg_commitments( + commitments: &BTreeMap, + ) -> Result { + let identifiers: BTreeSet<_> = commitments.keys().copied().collect(); + let commitments: Vec<_> = commitments.values().copied().collect(); + let group_commitment = sum_commitments(&commitments)?; + Self::from_commitment(&identifiers, &group_commitment) + } + + /// Derive verifying shares for each participant from a summed commitment. + /// + /// Ported from frost-core/src/keys.rs:751-763 + fn from_commitment( + identifiers: &BTreeSet, + commitment: &VerifiableSecretSharingCommitment, + ) -> Result { + let verifying_shares: BTreeMap<_, _> = identifiers + .iter() + .map(|id| (*id, VerifyingShare::from_commitment(*id, commitment))) + .collect(); + Ok(Self::new( + verifying_shares, + VerifyingKey::from_commitment(commitment)?, + )) + } +} + +/// Evaluate a polynomial using Horner's method. +/// +/// Given coefficients `[a_0, a_1, ..., a_{t-1}]`, computes +/// `a_0 + a_1 * x + a_2 * x^2 + ... + a_{t-1} * x^{t-1}`. +/// +/// Ported from frost-core/src/keys.rs:579-595 +fn evaluate_polynomial(identifier: Identifier, coefficients: &[Scalar]) -> Scalar { + let mut value = Scalar::ZERO; + let x = identifier.to_scalar(); + + for coeff in coefficients.iter().skip(1).rev() { + value = value + *coeff; + value = value * x; + } + value = value + + *coefficients + .first() + .expect("coefficients must have at least one element"); + value +} + +/// Evaluate the VSS verification equation at `identifier`. +/// +/// Computes `sum_{k=0}^{t-1} commitment[k] * identifier^k`. +/// +/// Ported from frost-core/src/keys.rs:602-615 +fn evaluate_vss( + identifier: Identifier, + commitment: &VerifiableSecretSharingCommitment, +) -> G1Projective { + let i = identifier.to_scalar(); + + let (_, result) = commitment.0.iter().fold( + (Scalar::ONE, G1Projective::identity()), + |(i_to_the_k, sum_so_far), comm_k| { + (i * i_to_the_k, sum_so_far + comm_k.value() * i_to_the_k) + }, + ); + result +} + +/// Sum multiple participants' commitments element-wise. +/// +/// Given commitments from n participants each of length t, produces a single +/// commitment of length t where each element is the sum of the corresponding +/// elements across all participants. +/// +/// Ported from frost-core/src/keys.rs:38-62 +fn sum_commitments( + commitments: &[&VerifiableSecretSharingCommitment], +) -> Result { + let mut group_commitment = vec![ + CoefficientCommitment::new(G1Projective::identity()); + commitments + .first() + .ok_or(FrostCoreError::IncorrectNumberOfCommitments)? + .0 + .len() + ]; + for commitment in commitments { + for (i, c) in group_commitment.iter_mut().enumerate() { + *c = CoefficientCommitment::new( + c.value() + + commitment + .0 + .get(i) + .ok_or(FrostCoreError::IncorrectNumberOfCommitments)? + .value(), + ); + } + } + Ok(VerifiableSecretSharingCommitment(group_commitment)) +} + +/// Validate that (min_signers, max_signers) form a valid pair. +/// +/// Ported from frost-core/src/keys.rs:798-815 +pub fn validate_num_of_signers(min_signers: u16, max_signers: u16) -> Result<(), FrostCoreError> { + if min_signers < 2 { + return Err(FrostCoreError::InvalidMinSigners); + } + if max_signers < 2 { + return Err(FrostCoreError::InvalidMaxSigners); + } + if min_signers > max_signers { + return Err(FrostCoreError::InvalidMinSigners); + } + Ok(()) +} diff --git a/crates/frost-bls12-381-g1/src/kryptology.rs b/crates/frost-bls12-381-g1/src/kryptology.rs new file mode 100644 index 00000000..4d479288 --- /dev/null +++ b/crates/frost-bls12-381-g1/src/kryptology.rs @@ -0,0 +1,601 @@ +//! Kryptology-compatible DKG for interoperability with Go's Coinbase Kryptology +//! FROST DKG. +//! +//! This module implements the same DKG protocol as +//! `github.com/coinbase/kryptology/pkg/dkg/frost`, which differs from the +//! standard FROST DKG in frost-core in the hash-to-scalar construction, +//! challenge preimage format, proof representation, and round structure. +//! +//! The output types ([`KeyPackage`], [`PublicKeyPackage`]) are standard +//! frost-core types usable with frost-core's signing protocol. + +use alloc::collections::BTreeMap; +use alloc::{vec, vec::Vec}; + +use blst::*; +use rand_core::{CryptoRng, RngCore}; +use sha2::{Digest, Sha256}; + +use super::*; + +/// Errors from the kryptology-compatible DKG. +#[derive(Debug)] +pub enum DkgError { + /// Participant ID is zero or out of range. + InvalidParticipantId(u32), + /// Two or more partial signatures share the same identifier. + DuplicateIdentifier(u32), + /// Fewer partial signatures than the threshold were provided. + InsufficientSigners, + /// Invalid number of signers. + InvalidSignerCount, + /// Invalid proof of knowledge from a specific participant. + InvalidProof { + /// The 1-indexed ID of the participant whose proof failed. + culprit: u32, + }, + /// Invalid Feldman share from a specific participant. + InvalidShare { + /// The 1-indexed ID of the participant whose share failed. + culprit: u32, + }, + /// Wrong number of received packages. + IncorrectPackageCount, + /// Failed to deserialize a scalar from wire format bytes. + InvalidScalar, + /// Failed to deserialize a G1 point from wire format bytes. + InvalidPoint, + /// Commitment count does not match threshold. + InvalidCommitmentCount { + /// The participant whose commitment count was wrong. + participant: u32, + }, + /// An error from frost-core. + FrostCoreError(FrostCoreError), +} + +impl From for DkgError { + fn from(e: FrostCoreError) -> Self { + DkgError::FrostCoreError(e) + } +} + +/// Kryptology Round 1 broadcast data matching Go's `frost.Round1Bcast`. +/// +/// Scalars (`wi`, `ci`) are in **big-endian** byte order to match Go's +/// kryptology wire format. Commitments are compressed G1 points (48 bytes). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Round1Bcast { + /// Feldman verifier commitments `[A_{i,0}, ..., A_{i,t-1}]`. + pub commitments: Vec<[u8; 48]>, + /// Proof-of-knowledge response scalar (big-endian). + pub wi: [u8; 32], + /// Proof-of-knowledge challenge scalar (big-endian). + pub ci: [u8; 32], +} + +/// Kryptology Round 2 broadcast data matching Go's `frost.Round2Bcast`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Round2Bcast { + /// The group verification key (compressed G1, 48 bytes). + pub verification_key: [u8; 48], + /// This participant's verification share (compressed G1, 48 bytes). + pub vk_share: [u8; 48], +} + +/// A Shamir secret share matching Go's `sharing.ShamirShare`. +/// +/// The `value` field is in **big-endian** byte order. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ShamirShare { + /// The share identifier (1-indexed participant ID). + pub id: u32, + /// The share value as big-endian scalar bytes. + pub value: [u8; 32], +} + +/// Secret state held by a participant between round 1 and round 2. +/// +/// # Security +/// +/// This MUST NOT be sent to other participants. +pub struct Round1Secret { + id: u32, + ctx: u8, + coefficients: Vec, + commitment: VerifiableSecretSharingCommitment, + threshold: u16, + max_signers: u16, +} + +impl Round1Secret { + /// Reconstruct a [`Round1Secret`] from wire-format data (e.g. a test + /// fixture) so that the standard [`round2`] function can be called. + /// + /// `own_share` is the big-endian scalar the participant computed for + /// itself. It is stored as the constant term of a zero polynomial so + /// that [`round2`]'s `from_coefficients` evaluation returns it + /// unchanged. + pub fn from_raw( + id: u32, + ctx: u8, + threshold: u16, + max_signers: u16, + own_share: &[u8; 32], + commitment_bytes: &[[u8; 48]], + ) -> Result { + let own_share_scalar = scalar_from_be(own_share)?; + let commitment = deserialize_commitment(id, threshold, commitment_bytes)?; + + let mut coefficients = vec![Scalar::ZERO; threshold as usize]; + coefficients[0] = own_share_scalar; + + Ok(Self { + id, + ctx, + coefficients, + commitment, + threshold, + max_signers, + }) + } +} + +/// Convert a `Scalar` to big-endian 32 bytes (Go's wire format). +pub fn scalar_to_be(s: &Scalar) -> [u8; 32] { + let mut bytes = s.to_bytes(); + bytes.reverse(); + bytes +} + +/// Convert big-endian 32 bytes to a `Scalar`. +pub fn scalar_from_be(bytes: &[u8; 32]) -> Result { + let mut le = *bytes; + le.reverse(); + Scalar::from_bytes(&le).ok_or(DkgError::InvalidScalar) +} + +/// RFC 9380 Section 5.3.1 using SHA-256 +pub fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { + const B_IN_BYTES: usize = 32; // SHA-256 output + const S_IN_BYTES: usize = 64; // SHA-256 block size + + let ell = len_in_bytes.div_ceil(B_IN_BYTES); + debug_assert!(ell <= 255 && len_in_bytes <= 65535 && dst.len() <= 255); + + let dst_prime_suffix = [dst.len() as u8]; + let l_i_b_str = [(len_in_bytes >> 8) as u8, (len_in_bytes & 0xff) as u8]; + + // b_0 = H(Z_pad || msg || l_i_b_str || I2OSP(0,1) || DST_prime) + let mut h0 = Sha256::new(); + h0.update([0u8; S_IN_BYTES]); + h0.update(msg); + h0.update(l_i_b_str); + h0.update([0u8]); + h0.update(dst); + h0.update(dst_prime_suffix); + let b_0: [u8; 32] = h0.finalize().into(); + + // b_1 = H(b_0 || I2OSP(1,1) || DST_prime) + let mut h1 = Sha256::new(); + h1.update(b_0); + h1.update([1u8]); + h1.update(dst); + h1.update(dst_prime_suffix); + let b_1: [u8; 32] = h1.finalize().into(); + + let mut out = Vec::with_capacity(ell * B_IN_BYTES); + out.extend_from_slice(&b_1); + + let mut b_prev = b_1; + for i in 2..=ell { + let mut xored = [0u8; 32]; + for j in 0..32 { + xored[j] = b_0[j] ^ b_prev[j]; + } + let mut hi = Sha256::new(); + hi.update(xored); + hi.update([i as u8]); + hi.update(dst); + hi.update(dst_prime_suffix); + let b_i: [u8; 32] = hi.finalize().into(); + out.extend_from_slice(&b_i); + b_prev = b_i; + } + + out.truncate(len_in_bytes); + out +} + +/// Kryptology hash-to-scalar. +/// See: https://github.com/coinbase/kryptology/blob/eef703320df46f97e86ead4eff178b095181b0ec/pkg/core/curves/bls12381_curve.go#L50 +const KRYPTOLOGY_DST: &[u8] = b"BLS12381_XMD:SHA-256_SSWU_RO_"; + +/// Hash to scalar using kryptology's ExpandMsgXmd construction. +/// +/// `ExpandMsgXmd(SHA-256, msg, DST, 48)` -> reverse bytes -> pad to 64 -> +/// `Scalar::from_bytes_wide`. +fn kryptology_hash_to_scalar(msg: &[u8]) -> Scalar { + let xmd = expand_msg_xmd(msg, KRYPTOLOGY_DST, 48); + let mut reversed = [0u8; 48]; + reversed.copy_from_slice(&xmd); + reversed.reverse(); + let mut wide = [0u8; 64]; + wide[..48].copy_from_slice(&reversed); + Scalar::from_bytes_wide(&wide) +} + +/// Compute the DKG challenge matching kryptology's format. +/// +/// Preimage = `byte(id) || byte(ctx) || A_{i,0}.compressed || R.compressed` +/// (98 bytes). +fn kryptology_challenge(id: u8, ctx: u8, commitment_0: &G1Projective, r: &G1Projective) -> Scalar { + let mut preimage = Vec::with_capacity(98); + preimage.push(id); + preimage.push(ctx); + preimage.extend_from_slice(&G1Affine::from(commitment_0).to_compressed()); + preimage.extend_from_slice(&G1Affine::from(r).to_compressed()); + kryptology_hash_to_scalar(&preimage) +} + +fn deserialize_commitment( + participant: u32, + threshold: u16, + commitments: &[[u8; 48]], +) -> Result { + if commitments.len() != threshold as usize { + return Err(DkgError::InvalidCommitmentCount { participant }); + } + + VerifiableSecretSharingCommitment::from_commitments(commitments).ok_or(DkgError::InvalidPoint) +} + +/// Perform Round 1 of the kryptology-compatible DKG. +/// +/// Generates the secret polynomial, Feldman commitments, Schnorr +/// proof-of-knowledge, and pre-computes Shamir shares for all other +/// participants. +/// +/// # Arguments +/// - `id`: This participant's 1-indexed identifier (1..=max_signers). +/// - `threshold`: Minimum number of signers (t). +/// - `max_signers`: Total number of signers (n). +/// - `ctx`: DKG context byte (typically 0). +/// - `rng`: Cryptographic RNG. +pub fn round1( + id: u32, + threshold: u16, + max_signers: u16, + ctx: u8, + rng: &mut R, +) -> Result<(Round1Bcast, BTreeMap, Round1Secret), DkgError> { + // Kryptology encodes participant identifiers into a single byte. + if max_signers > u8::MAX as u16 { + return Err(DkgError::InvalidSignerCount); + } + + validate_num_of_signers(threshold, max_signers)?; + + if id == 0 || id > max_signers as u32 { + return Err(DkgError::InvalidParticipantId(id)); + } + + // Generate random polynomial coefficients [a_0, ..., a_{t-1}] + let coefficients: Vec = (0..threshold).map(|_| Scalar::random(&mut *rng)).collect(); + + // Feldman commitments: A_{i,k} = a_{i,k} * G + let commitment_points: Vec = coefficients + .iter() + .map(|c| G1Projective::generator() * *c) + .collect(); + + let commitment = { + let cc: Vec = commitment_points + .iter() + .map(|p| CoefficientCommitment::new(*p)) + .collect(); + VerifiableSecretSharingCommitment::new(cc) + }; + + // Schnorr proof of knowledge: sample nonce k, compute R = k*G + let k = loop { + let s = Scalar::random(&mut *rng); + if s != Scalar::ZERO { + break s; + } + }; + let r_point = G1Projective::generator() * k; + let ci = kryptology_challenge(id as u8, ctx, &commitment_points[0], &r_point); + let wi = k + coefficients[0] * ci; + + // Pre-compute Shamir shares for every other participant + let mut shares = BTreeMap::new(); + for j in 1..=max_signers as u32 { + if j == id { + continue; + } + let j_id = Identifier::from_u32(j)?; + let share_scalar = SigningShare::from_coefficients(&coefficients, j_id).to_scalar(); + shares.insert( + j, + ShamirShare { + id: j, + value: scalar_to_be(&share_scalar), + }, + ); + } + + let bcast = Round1Bcast { + commitments: commitment_points + .iter() + .map(|p| G1Affine::from(p).to_compressed()) + .collect(), + wi: scalar_to_be(&wi), + ci: scalar_to_be(&ci), + }; + + let secret = Round1Secret { + id, + ctx, + coefficients, + commitment, + threshold, + max_signers, + }; + + Ok((bcast, shares, secret)) +} + +/// Perform Round 2 of the kryptology-compatible DKG. +/// +/// Verifies all received Round 1 broadcasts (proof-of-knowledge + Feldman +/// verification), aggregates received Shamir shares, and produces the final +/// key material. +/// +/// # Arguments +/// - `secret`: The [`Round1Secret`] from this participant's [`round1`] call. +/// - `received_bcasts`: Map from source participant ID to their [`Round1Bcast`]. +/// - `received_shares`: Map from source participant ID to the [`ShamirShare`] +/// they sent us. +pub fn round2( + secret: Round1Secret, + received_bcasts: &BTreeMap, + received_shares: &BTreeMap, +) -> Result<(Round2Bcast, KeyPackage, PublicKeyPackage), DkgError> { + let expected = (secret.max_signers - 1) as usize; + if received_bcasts.len() != expected || received_shares.len() != expected { + return Err(DkgError::IncorrectPackageCount); + } + + let own_identifier = Identifier::from_u32(secret.id)?; + let own_share_scalar = + SigningShare::from_coefficients(&secret.coefficients, own_identifier).to_scalar(); + + let mut peer_commitments: BTreeMap = + BTreeMap::new(); + let mut share_sum = Scalar::ZERO; + + for (&sender_id, bcast) in received_bcasts { + let sender_commitment = + deserialize_commitment(sender_id, secret.threshold, &bcast.commitments)?; + let a0 = sender_commitment.coefficients()[0].value(); + + // Verify proof of knowledge + let wi = scalar_from_be(&bcast.wi)?; + let ci = scalar_from_be(&bcast.ci)?; + + // Reconstruct R' = Wi*G - Ci*A_{j,0} + let r_reconstructed = G1Projective::generator() * wi - a0 * ci; + let ci_check = kryptology_challenge(sender_id as u8, secret.ctx, &a0, &r_reconstructed); + if ci_check != ci { + return Err(DkgError::InvalidProof { culprit: sender_id }); + } + + // Verify Feldman share + let share = received_shares + .get(&sender_id) + .ok_or(DkgError::IncorrectPackageCount)?; + if share.id != secret.id { + return Err(DkgError::InvalidShare { culprit: sender_id }); + } + let share_scalar = scalar_from_be(&share.value)?; + + let signing_share = SigningShare::new(share_scalar); + let secret_share = + SecretShare::new(own_identifier, signing_share, sender_commitment.clone()); + secret_share + .verify() + .map_err(|_| DkgError::InvalidShare { culprit: sender_id })?; + + share_sum = share_sum + share_scalar; + + let sender_identifier = Identifier::from_u32(sender_id)?; + peer_commitments.insert(sender_identifier, sender_commitment); + } + + let total_scalar = own_share_scalar + share_sum; + + let signing_share = SigningShare::new(total_scalar); + let verifying_share_element = G1Projective::generator() * total_scalar; + let verifying_share = VerifyingShare::new(verifying_share_element); + + // Build PublicKeyPackage from all participants' commitments + peer_commitments.insert(own_identifier, secret.commitment); + let commitment_refs: BTreeMap = + peer_commitments.iter().map(|(id, c)| (*id, c)).collect(); + let public_key_package = PublicKeyPackage::from_dkg_commitments(&commitment_refs)?; + + let verifying_key = *public_key_package.verifying_key(); + + let key_package = KeyPackage::new( + own_identifier, + signing_share, + verifying_share, + verifying_key, + secret.threshold, + ); + + // Serialize Round2Bcast + let vk_element = verifying_key.to_element(); + let bcast = Round2Bcast { + verification_key: G1Affine::from(vk_element).to_compressed(), + vk_share: G1Affine::from(verifying_share_element).to_compressed(), + }; + + Ok((bcast, key_package, public_key_package)) +} + +/// Domain separation tag for Ethereum 2.0 BLS signatures (proof of possession scheme). +/// +/// Matches Go's `bls.NewSigEth2()` which uses `blsSignaturePopDst`. +pub const BLS_SIG_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; + +/// A BLS partial signature in G2, produced by a single signer's key share. +#[derive(Clone)] +pub struct BlsPartialSignature { + /// The signer's 1-indexed identifier (used as the Lagrange x-coordinate). + pub identifier: u32, + point: blst_p2, +} + +impl BlsPartialSignature { + /// Produce a BLS partial signature from a [`KeyPackage`] produced by + /// kryptology DKG. + /// + /// Computes `partial_sig = (key_package.signing_share) * H(msg)` where H hashes the message + /// to a G2 point using the Ethereum 2.0 DST. + /// + /// The `id` must be the original 1-indexed kryptology participant ID. + pub fn from_key_package(id: u32, key_package: &KeyPackage, msg: &[u8]) -> BlsPartialSignature { + let scalar = key_package.signing_share().to_scalar(); + { + let signing_share: &Scalar = &scalar; + let h_msg = hash_to_g2(msg); + BlsPartialSignature { + identifier: id, + point: p2_mult(&h_msg, signing_share), + } + } + } +} + +/// A complete BLS signature in G2 (96 bytes compressed). +#[derive(Clone)] +pub struct BlsSignature { + point: blst_p2, +} + +impl BlsSignature { + /// Serialize to 96-byte compressed G2 point. + pub fn to_bytes(&self) -> [u8; 96] { + let mut affine = blst_p2_affine::default(); + let mut out = [0u8; 96]; + unsafe { + blst_p2_to_affine(&mut affine, &self.point); + blst_p2_affine_compress(out.as_mut_ptr(), &affine); + } + out + } + + /// Combine BLS partial signatures via Lagrange interpolation at x = 0. + /// + /// Matches Go's `combineSigs` in + /// `kryptology/pkg/signatures/bls/bls_sig/usual_bls_sig.go`. + /// + /// Returns [`DkgError::InsufficientSigners`] if `min_signers < 2` or + /// fewer than `min_signers` partial signatures are provided. + pub fn from_partial_signatures( + min_signers: u16, + partial_sigs: &[BlsPartialSignature], + ) -> Result { + if min_signers < 2 || partial_sigs.len() < min_signers as usize { + return Err(DkgError::InsufficientSigners); + } + + // Check for duplicate identifiers + let mut seen = alloc::collections::BTreeSet::new(); + for ps in partial_sigs { + if !seen.insert(ps.identifier) { + return Err(DkgError::DuplicateIdentifier(ps.identifier)); + } + } + + let x_vals: Vec = partial_sigs + .iter() + .map(|ps| Scalar::from(ps.identifier as u64)) + .collect(); + + let mut combined = blst_p2::default(); + let mut first = true; + + for (i, ps) in partial_sigs.iter().enumerate() { + // Lagrange coefficient: L_i(0) = prod_{j!=i} ( x_j / (x_j - x_i) ) + let mut lambda = Scalar::ONE; + for (j, _) in partial_sigs.iter().enumerate() { + if i == j { + continue; + } + let num = x_vals[j]; + let den = x_vals[j] - x_vals[i]; + let den_inv = den.invert().ok_or(DkgError::InvalidSignerCount)?; + lambda = lambda * num * den_inv; + } + + let weighted = p2_mult(&ps.point, &lambda); + + if first { + combined = weighted; + first = false; + } else { + let mut tmp = blst_p2::default(); + unsafe { blst_p2_add_or_double(&mut tmp, &combined, &weighted) }; + combined = tmp; + } + } + + Ok(BlsSignature { point: combined }) + } + + /// Verify a BLS signature against a public key. + /// + /// Uses the Ethereum 2.0 BLS verification (pairing check) with the + /// standard DST. + pub fn verify(&self, verifying_key: &VerifyingKey, msg: &[u8]) -> bool { + let pk_affine = G1Affine::from(verifying_key.to_element()); + let pk = blst::min_pk::PublicKey::from(pk_affine.0); + + let mut sig_affine = blst_p2_affine::default(); + unsafe { blst_p2_to_affine(&mut sig_affine, &self.point) }; + let sig = blst::min_pk::Signature::from(sig_affine); + + sig.verify(true, msg, BLS_SIG_DST, &[], &pk, true) == blst::BLST_ERROR::BLST_SUCCESS + } +} + +/// Hash a message to a G2 point using the Ethereum 2.0 BLS DST. +fn hash_to_g2(msg: &[u8]) -> blst_p2 { + let mut out = blst_p2::default(); + unsafe { + blst_hash_to_g2( + &mut out, + msg.as_ptr(), + msg.len(), + BLS_SIG_DST.as_ptr(), + BLS_SIG_DST.len(), + core::ptr::null(), + 0, + ); + } + out +} + +/// Multiply a G2 point by a scalar. +fn p2_mult(point: &blst_p2, scalar: &Scalar) -> blst_p2 { + let mut s = blst_scalar::default(); + let mut out = blst_p2::default(); + unsafe { + blst_scalar_from_fr(&mut s, &scalar.0); + blst_p2_mult(&mut out, point, s.b.as_ptr(), 255); + } + out +} diff --git a/crates/frost-bls12-381-g1/src/lib.rs b/crates/frost-bls12-381-g1/src/lib.rs new file mode 100644 index 00000000..04825dcb --- /dev/null +++ b/crates/frost-bls12-381-g1/src/lib.rs @@ -0,0 +1,19 @@ +#![no_std] +#![allow(non_snake_case)] +#![deny(missing_docs)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] +#![doc = include_str!("../dkg.md")] + +extern crate alloc; + +pub mod curve; +pub mod frost_core; +pub mod kryptology; + +pub use curve::*; +pub use frost_core::*; +pub use rand_core; + +#[cfg(test)] +mod tests; diff --git a/crates/frost-bls12-381-g1/src/tests.rs b/crates/frost-bls12-381-g1/src/tests.rs new file mode 100644 index 00000000..08934fd7 --- /dev/null +++ b/crates/frost-bls12-381-g1/src/tests.rs @@ -0,0 +1,300 @@ +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use rand::rngs::StdRng; +use rand::SeedableRng; + +use crate::kryptology::{self, BlsPartialSignature}; + +#[test] +fn check_scalar_one_precomputed() { + let constant = crate::Scalar::ONE; + let computed = crate::Scalar::from(1u64); + assert_eq!(constant, computed); +} + +/// RFC 9380 Section 5.3.1 test vector for expand_msg_xmd with SHA-256. +/// DST = "QUUX-V01-CS02-with-expander-SHA256-128" +/// msg = "" (empty), len_in_bytes = 0x20 (32) +#[test] +fn check_expand_msg_xmd_rfc9380_vector() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; + let msg = b""; + let expected = + hex::decode("68a985b87eb6b46952128911f2a4412bbc302a9d759667f87f7a21d803f07235").unwrap(); + + let result = kryptology::expand_msg_xmd(msg, dst, 32); + assert_eq!(result, expected, "expand_msg_xmd empty message vector"); +} + +/// RFC 9380 test vector: msg = "abc", len = 32 +#[test] +fn check_expand_msg_xmd_rfc9380_abc() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; + let msg = b"abc"; + let expected = + hex::decode("d8ccab23b5985ccea865c6c97b6e5b8350e794e603b4b97902f53a8a0d605615").unwrap(); + + let result = kryptology::expand_msg_xmd(msg, dst, 32); + assert_eq!(result, expected, "expand_msg_xmd abc vector"); +} + +/// RFC 9380 test vector: msg = "", len = 0x80 (128 bytes) +#[test] +fn check_expand_msg_xmd_rfc9380_long_output() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; + let msg = b""; + let expected = hex::decode( + "af84c27ccfd45d41914fdff5df25293e221afc53d8ad2ac06d5e3e2948\ + 5dadbee0d121587713a3e0dd4d5e69e93eb7cd4f5df4cd103e188cf60c\ + b02edc3edf18eda8576c412b18ffb658e3dd6ec849469b979d444cf7b2\ + 6911a08e63cf31f9dcc541708d3491184472c2c29bb749d4286b004ceb\ + 5ee6b9a7fa5b646c993f0ced", + ) + .unwrap(); + + let result = kryptology::expand_msg_xmd(msg, dst, 128); + assert_eq!(result, expected, "expand_msg_xmd 128-byte output vector"); +} + +#[test] +fn check_kryptology_rejects_more_than_255_signers() { + let mut rng = StdRng::seed_from_u64(42); + let result = kryptology::round1(1, 2, 256, 0, &mut rng); + + assert!(matches!( + result, + Err(kryptology::DkgError::InvalidSignerCount) + )); +} + +#[test] +fn check_kryptology_accepts_255_signers_boundary() { + let mut rng = StdRng::seed_from_u64(4242); + let (_bcast, shares, _secret) = kryptology::round1(1, 2, 255, 9, &mut rng) + .expect("255 signers should remain within kryptology's u8 transport limit"); + + assert_eq!(shares.len(), 254); + assert!(shares.contains_key(&255)); +} + +/// Full DKG round-trip: 3-of-3 DKG, then BLS threshold sign and verify. +#[test] +fn check_kryptology_bls_round_trip_3_of_3() { + let mut rng = StdRng::seed_from_u64(42); + let threshold = 3u16; + let max_signers = 3u16; + let ctx = 0u8; + + let mut bcasts: BTreeMap = BTreeMap::new(); + let mut all_shares: BTreeMap> = BTreeMap::new(); + let mut secrets: BTreeMap = BTreeMap::new(); + + for id in 1..=max_signers as u32 { + let (bcast, shares, secret) = kryptology::round1(id, threshold, max_signers, ctx, &mut rng) + .expect("round1 should succeed"); + bcasts.insert(id, bcast); + secrets.insert(id, secret); + + for (&target_id, share) in &shares { + all_shares + .entry(target_id) + .or_default() + .insert(id, share.clone()); + } + } + + // --- Round 2: each participant verifies + aggregates --- + let mut key_packages = BTreeMap::new(); + let mut public_key_packages = Vec::new(); + let mut round2_bcasts = BTreeMap::new(); + + for id in 1..=max_signers as u32 { + // Collect broadcasts from everyone except ourselves + let received_bcasts: BTreeMap = bcasts + .iter() + .filter(|(&k, _)| k != id) + .map(|(&k, v)| (k, v.clone())) + .collect(); + + let received_shares = all_shares.remove(&id).unwrap(); + let secret = secrets.remove(&id).unwrap(); + + let (r2_bcast, key_package, pub_package) = + kryptology::round2(secret, &received_bcasts, &received_shares) + .expect("round2 should succeed"); + + round2_bcasts.insert(id, r2_bcast); + key_packages.insert(id, key_package); + public_key_packages.push(pub_package); + } + + // All participants should agree on the group verification key + let vk = public_key_packages[0].verifying_key(); + for pkg in &public_key_packages[1..] { + assert_eq!( + vk, + pkg.verifying_key(), + "all participants must agree on the group key" + ); + } + + // All Round2Bcast should carry the same verification_key + let vk_bytes = round2_bcasts[&1].verification_key; + for (&id, bcast) in &round2_bcasts { + assert_eq!( + bcast.verification_key, vk_bytes, + "participant {id} round2 broadcast has different group key" + ); + } + + // BLS sign with all signers (t-of-t) + let message = b"test message"; + + let partial_sigs: Vec<_> = key_packages + .keys() + .map(|&id| BlsPartialSignature::from_key_package(id, &key_packages[&id], message)) + .collect(); + + let signature = kryptology::BlsSignature::from_partial_signatures(threshold, &partial_sigs) + .expect("BLS signature combination should succeed"); + + assert!( + signature.verify(&vk, message), + "3-of-3 BLS threshold signature should verify" + ); +} + +/// 2-of-3 DKG then BLS threshold signing (Ethereum 2.0 compatible). +#[test] +fn check_kryptology_bls_round_trip_2_of_3() { + let mut rng = StdRng::seed_from_u64(123); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + // Round 1 + let mut bcasts: BTreeMap = BTreeMap::new(); + let mut all_shares: BTreeMap> = BTreeMap::new(); + let mut secrets: BTreeMap = BTreeMap::new(); + + for id in 1..=max_signers as u32 { + let (bcast, shares, secret) = + kryptology::round1(id, threshold, max_signers, ctx, &mut rng).unwrap(); + bcasts.insert(id, bcast); + secrets.insert(id, secret); + for (&target_id, share) in &shares { + all_shares + .entry(target_id) + .or_default() + .insert(id, share.clone()); + } + } + + // Round 2 + let mut key_packages = BTreeMap::new(); + let mut public_key_packages = Vec::new(); + + for id in 1..=max_signers as u32 { + let received_bcasts: BTreeMap<_, _> = bcasts + .iter() + .filter(|(&k, _)| k != id) + .map(|(&k, v)| (k, v.clone())) + .collect(); + let received_shares = all_shares.remove(&id).unwrap(); + let secret = secrets.remove(&id).unwrap(); + + let (_r2_bcast, key_package, pub_package) = + kryptology::round2(secret, &received_bcasts, &received_shares).unwrap(); + key_packages.insert(id, key_package); + public_key_packages.push(pub_package); + } + + // BLS sign with only participants 1 and 2 (threshold = 2) + let message = b"threshold signing"; + let signers: [u32; 2] = [1, 2]; + + let partial_sigs: Vec<_> = signers + .iter() + .map(|&id| { + kryptology::BlsPartialSignature::from_key_package(id, &key_packages[&id], message) + }) + .collect(); + + let signature = kryptology::BlsSignature::from_partial_signatures(threshold, &partial_sigs) + .expect("BLS signature combination should succeed"); + + let vk = public_key_packages[0].verifying_key(); + assert!( + signature.verify(&vk, message), + "BLS threshold signature should verify" + ); + + // Verify wrong message fails + assert!( + !signature.verify(&vk, b"wrong message"), + "BLS signature should not verify against a different message" + ); +} + +/// Verify that an invalid proof is caught in round2. +#[test] +fn check_kryptology_invalid_proof_rejected() { + let mut rng = StdRng::seed_from_u64(99); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (mut bcast1, shares1, _secret1) = + kryptology::round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast2, _shares2, secret2) = + kryptology::round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + let (bcast3, shares3, _secret3) = + kryptology::round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); + + // Corrupt participant 1's proof (flip LSB of ci, keeping it a valid scalar) + bcast1.ci[31] ^= 0x01; + + // Participant 2 should reject participant 1's proof + let received_bcasts: BTreeMap = + [(1, bcast1.clone()), (3, bcast3.clone())].into(); + let received_shares: BTreeMap = + [(1, shares1[&2].clone()), (3, shares3[&2].clone())].into(); + + let result = kryptology::round2(secret2, &received_bcasts, &received_shares); + assert!(result.is_err()); + match result.unwrap_err() { + kryptology::DkgError::InvalidProof { culprit } => assert_eq!(culprit, 1), + other => panic!("expected InvalidProof, got {other:?}"), + } +} + +/// Verify that a share addressed to the wrong participant is rejected in round2. +#[test] +fn check_kryptology_share_id_mismatch_rejected() { + let mut rng = StdRng::seed_from_u64(42); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (bcast1, shares1, _secret1) = + kryptology::round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast2, _shares2, secret2) = + kryptology::round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + let (bcast3, shares3, _secret3) = + kryptology::round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); + + let received_bcasts: BTreeMap = [(1, bcast1), (3, bcast3)].into(); + + let mut wrong_share = shares1[&2].clone(); + wrong_share.id = 3; + let received_shares: BTreeMap = + [(1, wrong_share), (3, shares3[&2].clone())].into(); + + let result = kryptology::round2(secret2, &received_bcasts, &received_shares); + assert!(result.is_err()); + match result.unwrap_err() { + kryptology::DkgError::InvalidShare { culprit } => assert_eq!(culprit, 1), + other => panic!("expected InvalidShare, got {other:?}"), + } +} diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/2-of-3-ctx-0.json b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/2-of-3-ctx-0.json new file mode 100644 index 00000000..cee3153e --- /dev/null +++ b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/2-of-3-ctx-0.json @@ -0,0 +1,98 @@ +{ + "scenario": "2-of-3-ctx-0", + "threshold": 2, + "max_signers": 3, + "ctx": 0, + "participants": [ + { + "id": 1, + "own_share": "6fe7a63102bb215f2e63c80e34a3d566272391470057cc18cb2b9f7a895d446f", + "round1_bcast": { + "commitments": [ + "83d8ecdc5a7d2b003537db556b72d227939aa8aaf828cb3c815938532ec5c83e61de12417b5dbf632e574fc94f71f6a8", + "9699520ce41c1a7cea3e65a0fd38bb605908590fb436b50ff0463ea0695c7d13dbe6054949774391aeacf09ca8a093cf" + ], + "wi": "1924dfa0b455ec631a7a528f7efe3f8db92a5a4f558f95d5069299fd1a1b5fd6", + "ci": "169cee884e7a8d3e1aeafc96fc6f24465808f7ac7eb7ac1fbfb066ba2c024087" + }, + "shares_sent": [ + { + "to": 2, + "id": 2, + "value": "5525c01444985e474b3b657c3f0bd30e082a7f671601956ebc08317b42ebeb88" + }, + { + "to": 3, + "id": 3, + "value": "3a63d9f786759b2f681302ea4973d0b5e9316d872bab5ec4ace4c37bfc7a92a1" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "a8b196cd671819853edff76cb77b4fa8640a92d76e238ecf33fe70144ffc16885fb9d2f4a57d22232c7925e51d1bffc3", + "vk_share": "8dc83405eb8edc0e88c38b13223b16d3c3bb58e3c6f273c32c478065de8faa177dfc38cacda76688adfcee318291b464", + "signing_share": "15c87b3e2d2d32333cf62de8bfeaaceeaaac521886eccca0f255d093680108f7" + } + }, + { + "id": 2, + "own_share": "4e065ffa61ad2365a33d42190e91c85bd8661934ffaf23db147252909faa05a6", + "round1_bcast": { + "commitments": [ + "a8cd4313241b0433a155fcf367c22ae226e29fc954ab9e30838ffdc2bc6a4df7baca24a05fd9bbb38f99e9d558a21cf7", + "95cd8b8cf649da7090cf8e562a956f37c32e342cc8e5e18470227f71811493472e02dd60ced839b9ea4accc8421c3112" + ], + "wi": "2c6af59835a5ea910aec7b411432b13a78c221caae7258340e0a9a9ea35eb47f", + "ci": "34fd58ac0868df644430c56faf7d849c46f54986597ff55e09bf29b0dcf158cc" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "43550d90fd0969c2b8badc3657501399314b9ae4fe96e191aae5ca625292b176" + }, + { + "to": 3, + "id": 3, + "value": "58b7b263c650dd088dbfa7fbc5d37d1e7f80978500c766247dfedabeecc159d6" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "a8b196cd671819853edff76cb77b4fa8640a92d76e238ecf33fe70144ffc16885fb9d2f4a57d22232c7925e51d1bffc3", + "vk_share": "a7882f0fc549be8adaacc32a4d0a34fa3cf0d2a1363e09045db47a4017c99a6377aa387acae343a01c110d7c54981981", + "signing_share": "18d20bf9d4bd67bbfda0e15f3a2f7d105992491a1a65d9c30fa57bdd6a0ca2f1" + } + }, + { + "id": 3, + "own_share": "709b5f0082c21f9d2eec99ffb870af68474183158168d9fa021188ea82dc5076", + "round1_bcast": { + "commitments": [ + "a4c9ff28683d0dbc12f9e38b6850c6e56f91fada9749fe24530824021b9b1f5740b5eedf925a99afa420e30727f52764", + "b3c299d13d0032d04b321d8ec112eb7609030d90d2dca421c95288dd224fc8088ae4aac1da5266fae179575be51e272b" + ], + "wi": "07651a18f871afe03c190d594061ca5baebc352be98ad8a7385b3efc9a9d2630", + "ci": "240ef8016b73c0d6c98ce323141e131b7f46ed6d00716113eee75af24fb11c92" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "4a67162280a3a1a1bc4b39b4473a73f9f9b86df287fad6f47c4466b48c111314" + }, + { + "to": 2, + "id": 2, + "value": "5d813a9181b2e09f759be9d9ffd591b1207cf88404b1d8773f2af7cf8776b1c5" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "a8b196cd671819853edff76cb77b4fa8640a92d76e238ecf33fe70144ffc16885fb9d2f4a57d22232c7925e51d1bffc3", + "vk_share": "b0642f02c228b4a13b87dbf6053be2aec4f162848187defb7b674f71561eb1af4c6da58616f39e0ac84eb9ff5a2a7fe6", + "signing_share": "1bdb9cb57c4d9d44be4b94d5b4744d320878401baddee6e52cf527276c183ceb" + } + } + ] +} diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/3-of-3-ctx-0.json b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/3-of-3-ctx-0.json new file mode 100644 index 00000000..675e33bd --- /dev/null +++ b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/3-of-3-ctx-0.json @@ -0,0 +1,101 @@ +{ + "scenario": "3-of-3-ctx-0", + "threshold": 3, + "max_signers": 3, + "ctx": 0, + "participants": [ + { + "id": 1, + "own_share": "0492e87deb89041f39fb02ef29e34a5e56b2866e34d1d0bc424cb782dc8eec01", + "round1_bcast": { + "commitments": [ + "96fe8b3ef10133d9e30991a9347a08d9d684e5f0c22e960877993e7f8cc1965fcd525c654c1cf28cf8b14cc96abaa046", + "b8ee7a50aa1fdbd81b60644ee2f256f11b14165b5be57b8e4091bd55ccd2fe60536ede23677e96d7e5328dc60d00f097", + "b0463740d12d80a834d87f66f50dca6b5860872092e5e6cb1d438488a02f1c016d017a009ff606956862e9a341abe9a9" + ], + "wi": "3b07c49d669794cbb3b09c338975ccc369ba1b9926e479cdb93f3c537f04ddc9", + "ci": "445a769c5e06a2ce71b7ea4664ef197837d11a342ac4e618b7f106cdd79d5b0a" + }, + "shares_sent": [ + { + "to": 2, + "id": 2, + "value": "0e152b19bf45bbe122f40593f4b47b6cd196a468e3a8e22e949c7807f16b4196" + }, + { + "to": 3, + "id": 3, + "value": "6d1f077dc51978f69ffff1f8dbcff11337942cb3ba344891f368626aea771c65" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "b67cc957b409abf46072441ca39f38f3c4475f83054a70ae6a622226fcfe12112782400d3213d8925b5ee40a1dc79514", + "vk_share": "855e8c7950a705f29afe4190c0387e3eab5a5b4096ac89e4d7bc9ef5ab29e0833f478b389e3d3e90b4d5ddc576420a88", + "signing_share": "732e6f52828c69371fb185c8ce16a516cb6a8100616b3dd57c10c09c0a827b43" + } + }, + { + "id": 2, + "own_share": "3c9d784f782cf9a7fe2ce1a66bb3704bf971d5b2e287ee78234abb3bb6329862", + "round1_bcast": { + "commitments": [ + "b497486677c8e128032d80378a0f4bcdb4b355e461e42bd627c4ef7d9b5a4f9f84058e82908d715c143381c3c3502f98", + "957d0a403152ef8ddefa27198ef5b29657ea1aa1b37b0604ab15448dfec2c3bb2693ec0bd1871aa6edfa2bfc70647124", + "a409004071377662c431e7d4600f69bc2b1c4f17ff309746361dba309ea15b411b4045f8a2d4116b9e69b0e281e5e1e7" + ], + "wi": "39efdb83bc0c0721c79f70098b51e0b9c600d22ad20768f75177cfde7ace7bf3", + "ci": "0d929e099f0aeaecd7bbf0d4e0c0bfbf83760ed7097868803dc6fc7947b32355" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "5ef15fc54fb3cd0413255d2ef5bf7e4557047a88e00b83274c9e667a0989ca18" + }, + { + "to": 3, + "id": 3, + "value": "15ca8516d0b92fcfb67ec74fd2c0bc195e27051506ba90c810f700145c20cafb" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "b67cc957b409abf46072441ca39f38f3c4475f83054a70ae6a622226fcfe12112782400d3213d8925b5ee40a1dc79514", + "vk_share": "b9da3e740ab266b479bc0c39b0ea8fda261ef24df18a34310e77b302d20275e7743b6f17cd9b9ed1052ea10f4924e4d0", + "signing_share": "2077c7d5feab13d2404d658b8273056c052fc6e7291d64a35e36c42d3e71462d" + } + }, + { + "id": 3, + "own_share": "495275c3d50c4b19f69c13c0c068ec146a62020e811bf717701527f80d0fb758", + "round1_bcast": { + "commitments": [ + "a3ff23a2c87d82db33ac84d8975bd687b0514600cf8a7ef48ffd76ef2596d424cf873f71c60e095069a19a0c91bd8927", + "972e6e4644c2e5af5883cc3cdbc0380cb9d53ee41854cacc7c19933a444d14d39c02dc410f616b001da236c9835d33bf", + "9440c02c432c6446fe41efea6d8990c7efd6707c7f9f1ec84efc0e2c7dfd5b6b77e9bd015e2056090eb553c270275eb9" + ], + "wi": "6249ec86b032596e931ba95b26e69d45ef323afe2bff6ed171005867df6d7d22", + "ci": "1daa00119cb5a1f83606a8808f2d2688f3712ed34effde90b8b67a2efaf2fe04" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "0faa270f474f9813d29125aaae73dc731db380094c8de9f1ed25a29f2469c52a" + }, + { + "to": 2, + "id": 2, + "value": "49b2cbbff0d5db91526656592bacf1b88de4f0ce62eaeffba64f90e896d36c36" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "b67cc957b409abf46072441ca39f38f3c4475f83054a70ae6a622226fcfe12112782400d3213d8925b5ee40a1dc79514", + "vk_share": "8ed40869c509af07504e08226464a475385154fc26ad4f989c814f36a6ded57e1e424f66c9ffddf2b1d2fa4717cd38b2", + "signing_share": "584e5b054141769819e0f5016557c13bac5f8fd4420c747274748a7853a79eb7" + } + } + ] +} diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/invalid-proof.json b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/invalid-proof.json new file mode 100644 index 00000000..2e49d3ce --- /dev/null +++ b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/invalid-proof.json @@ -0,0 +1,94 @@ +{ + "scenario": "invalid-proof", + "threshold": 2, + "max_signers": 3, + "ctx": 0, + "participants": [ + { + "id": 1, + "own_share": "36def1bba1e65144492c3e74a74749ff911be1a1d8abebd8276b735d31e80e87", + "round1_bcast": { + "commitments": [ + "8bfdaf557e7a365d9c75cd0b1ee2d4ba7e6f4f100c99f324cc3d5b1c0bb07b4b15f32468f87abce40f1daf1524b90761", + "adb59012564d5e7a935ffc7cc22f4e68692edeb8039097e39fa3e252d56317ec234acc584b36a7818bd770c92f2a0d41" + ], + "wi": "21ebef1eda9e73251c7947ce127b161d00b6e03814c040e216361478f31d5385", + "ci": "4a716c6b8ea9bca97b16f3d3b6f66c74d463520659593e2ee6aa17b5a3e0f002" + }, + "shares_sent": [ + { + "to": 2, + "id": 2, + "value": "3547e9c1608c58b0fd18839ac657fd1fa3e921c55d5ce906bf03fa414235d32c" + }, + { + "to": 3, + "id": 3, + "value": "33b0e1c71f32601db104c8c0e568b03fb6b661e8e20de635569c8125528397d1" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "971812af8f5b42b493625bcc4fa1e44acda41c94c6cbe9763674765365fa4bd8ca747c081c8e7c30a4acf089210d3b9a", + "vk_share": "b600b36660a6b68dea3d05c86028ff5056415b492a50e5188b73424d4b31e79dc3f8db3d185d5ea2f6c6367d4546c188", + "signing_share": "1e73c557eb8d575f0d45e760a34e456c3d10fac38a476a32af6162805a7391f5" + } + }, + { + "id": 2, + "own_share": "2c395cc0763fbbbc7862a4983bdd875285f07816b7ed371e52ad95598cfcd16d", + "round1_bcast": { + "commitments": [ + "83a588bc21634d57e260d6f39772f2ecfa7ffe6a2f7403cad88495ef831afef86da67d2b1b5cfa1945dfab4d8f647122", + "a01895c79ae4ecb6175b39a9050f3e796f326a7e816fca5525dd99338bb5639c0a161158996b868bb348d6d3222ae596" + ], + "wi": "726ac2fe13a7be09cadd2affa70f3c29d555b2e90db1f9734ae7acd71cf84f3b", + "ci": "66050fb2b10ebbaa84a07a45bac02142c495454e82b8541ab237ac82d904cc4c" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "31fa0036b6f62d76308ff64cc56c2103769089397bd308035687784b345d2924" + }, + { + "to": 3, + "id": 3, + "value": "2678b94a35894a02c03552e3b24eeda1955066f3f40766394ed3b267e59c79b6" + } + ], + "expected_round2": { + "kind": "invalid_proof", + "culprit": 1 + } + }, + { + "id": 3, + "own_share": "13adcf75fcad7d4892795687ce8272920a6e7a57abdd04fcd5ba212c414accd1", + "round1_bcast": { + "commitments": [ + "b457326e06db3e90e97bbce3d373687b0ac75d0c5d216a20f77c100d7b13d08bea2f5c071957291270cb8ce12d852c51", + "876205abfa05a57325946eae42fe461ca14272a2eb166590c96a6b208c8efe4c2ba4bb8fbc3633c8fcd3108e74d9e8d3" + ], + "wi": "3407279b3f7da5c153ade91e296a8824b8df875511bfbb80efa1d22fb255aca5", + "ci": "12a29d0aebf1d63ca8f9979c9f50fb91eef6cde0ad47d329f2a22b7ab16f3d25" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "29887ab8bc4e55ecc6c38aa7403cb26e892233eb35c6d256316e76d6f42e5a4b" + }, + { + "to": 2, + "id": 2, + "value": "1e9b25175c7de99aac9e7097875f928049c8572170d1eba983944c019abc938e" + } + ], + "expected_round2": { + "kind": "invalid_proof", + "culprit": 1 + } + } + ] +} diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/malformed-share-id.json b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/malformed-share-id.json new file mode 100644 index 00000000..cd0c4f17 --- /dev/null +++ b/crates/frost-bls12-381-g1/tests/kryptology_fixtures/malformed-share-id.json @@ -0,0 +1,96 @@ +{ + "scenario": "malformed-share-id", + "threshold": 2, + "max_signers": 3, + "ctx": 0, + "participants": [ + { + "id": 1, + "own_share": "5a003ff621865347e52d252e6d1be691b9431caf11585cdbbb4b770e848f1d5a", + "round1_bcast": { + "commitments": [ + "883e25f48cabc48002e04f7eb58482dbeed7aea6cb0af4160f1d39ce830036f534ee3def786e342d0aa0fe3504840b5a", + "a9ca86507b03fe948b81b2eedf8f692fa08a60ebf2fab5042fd4a314ad39e0aed933ebab3bdbd2167d39a4ea6ea4de7f" + ], + "wi": "3d72a1abdf3e71f5fbd0b18e1b4d7ccf21947d045ef88054474b6c62203b6ed0", + "ci": "032ee5e8bf5c4bf510daa1d434b9cfc448a935a8df10d8393ee9d32d3e9798e9" + }, + "shares_sent": [ + { + "to": 2, + "id": 3, + "value": "3b77bd501fc44e3302a2d22082d7c779e8780986ecfba8e817b37a08026365fd" + }, + { + "to": 3, + "id": 3, + "value": "1cef3aaa1e02491e20187f129893a86217acf65ec89ef4f4741b7d018037aea0" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "ab2761905e0a0400fc6b5b9b8a309bc68102cbaa9f54801f3bf1bd61072c6e21dca51d4cdb26ee16dd96583dadaf3a45", + "vk_share": "a248443c0eab2131e2430700a02fd0ea797856ffc407fb8c980aa4ea015455c3cad0799e4e16de3053db73fe1d17a2cc", + "signing_share": "2091fc8168c8e615be05d2382f9b940f95711e63c0d775d4ae9b4518feef321e" + } + }, + { + "id": 2, + "own_share": "1e8152a8e8d089fcb7eb4c8d88696c5a8ccaa96969e7873562ba53373d7c5b06", + "round1_bcast": { + "commitments": [ + "b87c923a2070b0dd6072593237b60cdc944c229c4b5b0b14b6124dbeda3ec7b572c844b26de0dbf462205c775dd2d041", + "a544de0ac6d1e324d877bc9430f57cd5a06127feb13d51de3be1c45b824e0816f066bd8bbec1ff40264001a7466de305" + ], + "wi": "22317987a9fde571a811960e109559c92c03cf2fb961935334be97f1636c0e8e", + "ci": "2d22b1dfeeb27df560dfb9fad565d747340d01df9e678d8be7a7edc64ba1e25b" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "4fcda7ddf2e27adef98bb60ceb024a961b06f73dd312b9f77bff6d95bcb3451c" + }, + { + "to": 3, + "id": 3, + "value": "6122a4c7085c1662a984bb162f726624524bff9800bab072497538d7be4570f1" + } + ], + "expected_round2": { + "kind": "invalid_share", + "culprit": 1 + } + }, + { + "id": 3, + "own_share": "681887db99f3584ee8d5b3c3ea980beae36f032b7a9f793acb2a0c985e90cf10", + "round1_bcast": { + "commitments": [ + "90b0825a65430965219c3d1356b065ac3fbc59c98917d82842ec614f933f87d6c54e96368febbb1e06136f40cc4ebd69", + "88e449375604235bf195f04bfdbc15811def80697916e0c0cbd78b85d7509004d722ac46d522a93f0e8d0f53cd4271c7" + ], + "wi": "33bae1ae3df865782681200578a54d295985c8015a8957942e8d8be7a53e9b0a", + "ci": "4998088840ea240616e56aef6150813b30958419e3eabe5e52ed8b139eae4626" + }, + "shares_sent": [ + { + "to": 1, + "id": 1, + "value": "5e9f6353a79b127f45c0a70ceac112f268a2527cdc6916ff77506072bdaccfaa" + }, + { + "to": 2, + "id": 2, + "value": "635bf597a0c73567174b2d686aac8f6ea608aad42b84481d213d36858e1ecf5d" + } + ], + "expected_round2": { + "kind": "success", + "verification_key": "ab2761905e0a0400fc6b5b9b8a309bc68102cbaa9f54801f3bf1bd61072c6e21dca51d4cdb26ee16dd96583dadaf3a45", + "vk_share": "95e4e1281513eafc4129a144fe689c120f8a81c1e77b62a510321be725fa081706d183dc1e4d74a59dfe9c00a2e1129d", + "signing_share": "723cbff996b43a877f3915e4a8fc426bf9aa551f43fac2a288bac2729d0deea0" + } + } + ] +} diff --git a/crates/frost-bls12-381-g1/tests/kryptology_interop.rs b/crates/frost-bls12-381-g1/tests/kryptology_interop.rs new file mode 100644 index 00000000..f1caf547 --- /dev/null +++ b/crates/frost-bls12-381-g1/tests/kryptology_interop.rs @@ -0,0 +1,262 @@ +use std::collections::BTreeMap; + +use frost_bls12_381_g1::kryptology; +use serde_json::Value; + +const FIXTURE_2_OF_3_CTX_0: &str = include_str!("./kryptology_fixtures/2-of-3-ctx-0.json"); +const FIXTURE_3_OF_3_CTX_0: &str = include_str!("./kryptology_fixtures/3-of-3-ctx-0.json"); +const FIXTURE_MALFORMED_SHARE_ID: &str = + include_str!("./kryptology_fixtures/malformed-share-id.json"); +const FIXTURE_INVALID_PROOF: &str = include_str!("./kryptology_fixtures/invalid-proof.json"); + +#[derive(Clone)] +struct FixtureParticipant { + id: u32, + own_share: [u8; 32], + round1_bcast: kryptology::Round1Bcast, + shares_sent: BTreeMap, + expected_round2: ExpectedRound2, +} + +#[derive(Clone)] +enum ExpectedRound2 { + Success { + verification_key: [u8; 48], + vk_share: [u8; 48], + signing_share: [u8; 32], + }, + Error { + kind: String, + culprit: u32, + }, +} + +struct FixtureScenario { + threshold: u16, + max_signers: u16, + ctx: u8, + participants: BTreeMap, +} + +#[test] +fn check_kryptology_fixture_round2_interop_2_of_3_ctx_0() { + replay_fixture(FIXTURE_2_OF_3_CTX_0, true); +} + +#[test] +fn check_kryptology_fixture_round2_interop_3_of_3_ctx_0() { + replay_fixture(FIXTURE_3_OF_3_CTX_0, true); +} + +#[test] +fn check_kryptology_fixture_round2_interop_malformed_share_id() { + replay_fixture(FIXTURE_MALFORMED_SHARE_ID, false); +} + +#[test] +fn check_kryptology_fixture_round2_interop_invalid_proof() { + replay_fixture(FIXTURE_INVALID_PROOF, false); +} + +fn replay_fixture(json: &str, require_group_signature: bool) { + let scenario = parse_fixture(json); + + let mut key_packages = BTreeMap::new(); + let mut public_key_packages = Vec::new(); + + for (&id, participant) in &scenario.participants { + let received_bcasts = scenario + .participants + .iter() + .filter(|&(&sender_id, _)| sender_id != id) + .map(|(&sender_id, sender)| (sender_id, sender.round1_bcast.clone())) + .collect(); + + let received_shares = scenario + .participants + .iter() + .filter(|&(&sender_id, _)| sender_id != id) + .map(|(&sender_id, sender)| (sender_id, sender.shares_sent[&id].clone())) + .collect(); + + let secret = kryptology::Round1Secret::from_raw( + participant.id, + scenario.ctx, + scenario.threshold, + scenario.max_signers, + &participant.own_share, + &participant.round1_bcast.commitments, + ) + .expect("Round1Secret::from_raw should succeed"); + let result = kryptology::round2(secret, &received_bcasts, &received_shares); + + match &participant.expected_round2 { + ExpectedRound2::Success { + verification_key, + vk_share, + signing_share, + } => { + let (round2_bcast, key_package, public_key_package) = + result.expect("round2 should succeed"); + assert_eq!(round2_bcast.verification_key, *verification_key); + assert_eq!(round2_bcast.vk_share, *vk_share); + assert_eq!( + kryptology::scalar_to_be(&key_package.signing_share().to_scalar()), + *signing_share, + ); + + key_packages.insert(id, key_package); + public_key_packages.push(public_key_package); + } + ExpectedRound2::Error { kind, culprit } => { + let err = result.expect_err("round2 should fail"); + match (kind.as_str(), err) { + ("invalid_share", kryptology::DkgError::InvalidShare { culprit: got }) => { + assert_eq!(got, *culprit); + } + ("invalid_proof", kryptology::DkgError::InvalidProof { culprit: got }) => { + assert_eq!(got, *culprit); + } + (expected, other) => panic!("expected {expected}, got {other:?}"), + } + } + } + } + + if !require_group_signature { + return; + } + + let vk = public_key_packages[0].verifying_key(); + for package in &public_key_packages[1..] { + assert_eq!(vk, package.verifying_key()); + } + + let message = b"kryptology fixture signing"; + + let partial_sigs: Vec<_> = key_packages + .iter() + .map(|(&id, kp)| kryptology::BlsPartialSignature::from_key_package(id, kp, message)) + .collect(); + + let signature = + kryptology::BlsSignature::from_partial_signatures(scenario.threshold, &partial_sigs) + .expect("BLS signature combination should succeed"); + + assert!( + signature.verify(&vk, message), + "fixture-derived BLS threshold signature should verify" + ); +} + +fn parse_fixture(json: &str) -> FixtureScenario { + let root: Value = serde_json::from_str(json).expect("fixture JSON should parse"); + let threshold = get_u64(&root, "threshold") as u16; + let max_signers = get_u64(&root, "max_signers") as u16; + let ctx = get_u64(&root, "ctx") as u8; + + let participants = get_array(&root, "participants") + .iter() + .map(parse_participant) + .map(|participant| (participant.id, participant)) + .collect(); + + FixtureScenario { + threshold, + max_signers, + ctx, + participants, + } +} + +fn parse_participant(value: &Value) -> FixtureParticipant { + let id = get_u64(value, "id") as u32; + let own_share = decode_hex_32(get_str(value, "own_share")); + let round1_bcast = parse_round1_bcast(get_value(value, "round1_bcast")); + + let shares_sent = get_array(value, "shares_sent") + .iter() + .map(|share| { + let recipient = get_u64(share, "to") as u32; + let wire = kryptology::ShamirShare { + id: get_u64(share, "id") as u32, + value: decode_hex_32(get_str(share, "value")), + }; + (recipient, wire) + }) + .collect(); + + let expected_round2 = parse_expected_round2(get_value(value, "expected_round2")); + + FixtureParticipant { + id, + own_share, + round1_bcast, + shares_sent, + expected_round2, + } +} + +fn parse_round1_bcast(value: &Value) -> kryptology::Round1Bcast { + let commitments = get_array(value, "commitments") + .iter() + .map(|commitment| decode_hex_48(commitment.as_str().expect("hex string"))) + .collect(); + + kryptology::Round1Bcast { + commitments, + wi: decode_hex_32(get_str(value, "wi")), + ci: decode_hex_32(get_str(value, "ci")), + } +} + +fn parse_expected_round2(value: &Value) -> ExpectedRound2 { + let kind = get_str(value, "kind"); + match kind { + "success" => ExpectedRound2::Success { + verification_key: decode_hex_48(get_str(value, "verification_key")), + vk_share: decode_hex_48(get_str(value, "vk_share")), + signing_share: decode_hex_32(get_str(value, "signing_share")), + }, + "invalid_share" | "invalid_proof" => ExpectedRound2::Error { + kind: String::from(kind), + culprit: get_u64(value, "culprit") as u32, + }, + other => panic!("unsupported expected_round2 kind: {other}"), + } +} + +fn get_value<'a>(value: &'a Value, key: &str) -> &'a Value { + value + .get(key) + .unwrap_or_else(|| panic!("missing key: {key}")) +} + +fn get_array<'a>(value: &'a Value, key: &str) -> &'a [Value] { + get_value(value, key) + .as_array() + .map(Vec::as_slice) + .unwrap_or_else(|| panic!("{key} should be an array")) +} + +fn get_str<'a>(value: &'a Value, key: &str) -> &'a str { + get_value(value, key) + .as_str() + .unwrap_or_else(|| panic!("{key} should be a string")) +} + +fn get_u64(value: &Value, key: &str) -> u64 { + get_value(value, key) + .as_u64() + .unwrap_or_else(|| panic!("{key} should be a u64")) +} + +fn decode_hex_32(hex_value: &str) -> [u8; 32] { + let bytes = hex::decode(hex_value).expect("hex should decode"); + bytes.try_into().expect("expected 32-byte value") +} + +fn decode_hex_48(hex_value: &str) -> [u8; 48] { + let bytes = hex::decode(hex_value).expect("hex should decode"); + bytes.try_into().expect("expected 48-byte value") +} diff --git a/crates/frost-bls12-381-g1/tests/kryptology_round_trip.rs b/crates/frost-bls12-381-g1/tests/kryptology_round_trip.rs new file mode 100644 index 00000000..46037d17 --- /dev/null +++ b/crates/frost-bls12-381-g1/tests/kryptology_round_trip.rs @@ -0,0 +1,111 @@ +use std::collections::BTreeMap; + +use frost_bls12_381_g1::kryptology; +use rand::{rngs::StdRng, SeedableRng}; + +/// Follows: https://github.com/coinbase/kryptology/blob/f129811df480d7c65115a20b0461c4e406bd28f2/test/frost_dkg/bls/main.go#L23 +/// +/// FROST DKG + BLS threshold signing (Ethereum 2.0 compatible). +/// This matches Go's signing flow: non-interactive BLS partial signatures +/// combined via Lagrange interpolation, verified with standard BLS pairings. +#[test] +fn check_kryptology_bls_round_trip_2_of_4_ctx_0() { + let mut rng = StdRng::seed_from_u64(20260410); + let threshold = 2u16; + let max_signers = 4u16; + let ctx = 0u8; + + let mut round1_bcasts = BTreeMap::new(); + let mut round1_shares: BTreeMap> = BTreeMap::new(); + let mut round1_secrets = BTreeMap::new(); + + for id in 1..=max_signers as u32 { + let (bcast, shares, secret) = kryptology::round1(id, threshold, max_signers, ctx, &mut rng) + .expect("round1 should succeed for each participant"); + + assert_eq!(shares.len(), (max_signers - 1) as usize); + for (&recipient_id, share) in &shares { + assert_eq!(share.id, recipient_id); + } + + round1_bcasts.insert(id, bcast); + round1_secrets.insert(id, secret); + + for (&recipient_id, share) in &shares { + round1_shares + .entry(recipient_id) + .or_default() + .insert(id, share.clone()); + } + } + + assert_eq!(round1_bcasts.len(), max_signers as usize); + assert_eq!(round1_shares.len(), max_signers as usize); + + let mut round2_bcasts = BTreeMap::new(); + let mut key_packages = BTreeMap::new(); + let mut public_key_packages = BTreeMap::new(); + + for id in 1..=max_signers as u32 { + let received_bcasts: BTreeMap = round1_bcasts + .iter() + .filter(|&(sender_id, _)| *sender_id != id) + .map(|(&sender_id, bcast)| (sender_id, bcast.clone())) + .collect(); + let received_shares = round1_shares + .remove(&id) + .expect("each participant should receive shares from all peers"); + let secret = round1_secrets + .remove(&id) + .expect("round1 secret should exist for each participant"); + + assert_eq!(received_bcasts.len(), (max_signers - 1) as usize); + assert_eq!(received_shares.len(), (max_signers - 1) as usize); + + let (round2_bcast, key_package, public_key_package) = + kryptology::round2(secret, &received_bcasts, &received_shares) + .expect("round2 should succeed for each participant"); + + round2_bcasts.insert(id, round2_bcast); + key_packages.insert(id, key_package); + public_key_packages.insert(id, public_key_package); + } + + let group_key = public_key_packages[&1].verifying_key(); + for (&id, public_key_package) in &public_key_packages { + assert_eq!( + public_key_package.verifying_key(), + group_key, + "participant {id} derived a different group verification key" + ); + } + + let verification_key_bytes = round2_bcasts[&1].verification_key; + for (&id, round2_bcast) in &round2_bcasts { + assert_eq!( + round2_bcast.verification_key, verification_key_bytes, + "participant {id} broadcast a different round2 verification key" + ); + } + + // BLS threshold signing (matches Go's main.go) + let message = b"All my bitcoin is stored here"; + let signing_participants = [1u32, 2u32]; + + let partial_sigs: Vec<_> = signing_participants + .iter() + .map(|&id| { + kryptology::BlsPartialSignature::from_key_package(id, &key_packages[&id], message) + }) + .collect(); + + assert_eq!(partial_sigs.len(), threshold as usize); + + let signature = kryptology::BlsSignature::from_partial_signatures(threshold, &partial_sigs) + .expect("BLS signature combination should succeed"); + + assert!( + signature.verify(&group_key, message), + "BLS threshold signature should verify against the group public key" + ); +} From cb9e16aec501466d8420bec288a298c5d12751b0 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:33:44 -0300 Subject: [PATCH 02/23] Rename dir --- crates/{frost-bls12-381-g1 => frost}/Cargo.toml | 0 crates/{frost-bls12-381-g1 => frost}/README.md | 0 crates/{frost-bls12-381-g1 => frost}/dkg.md | 0 crates/{frost-bls12-381-g1 => frost}/src/curve.rs | 0 crates/{frost-bls12-381-g1 => frost}/src/frost_core.rs | 0 crates/{frost-bls12-381-g1 => frost}/src/kryptology.rs | 0 crates/{frost-bls12-381-g1 => frost}/src/lib.rs | 0 crates/{frost-bls12-381-g1 => frost}/src/tests.rs | 0 .../tests/kryptology_fixtures/2-of-3-ctx-0.json | 0 .../tests/kryptology_fixtures/3-of-3-ctx-0.json | 0 .../tests/kryptology_fixtures/invalid-proof.json | 0 .../tests/kryptology_fixtures/malformed-share-id.json | 0 crates/{frost-bls12-381-g1 => frost}/tests/kryptology_interop.rs | 0 .../{frost-bls12-381-g1 => frost}/tests/kryptology_round_trip.rs | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename crates/{frost-bls12-381-g1 => frost}/Cargo.toml (100%) rename crates/{frost-bls12-381-g1 => frost}/README.md (100%) rename crates/{frost-bls12-381-g1 => frost}/dkg.md (100%) rename crates/{frost-bls12-381-g1 => frost}/src/curve.rs (100%) rename crates/{frost-bls12-381-g1 => frost}/src/frost_core.rs (100%) rename crates/{frost-bls12-381-g1 => frost}/src/kryptology.rs (100%) rename crates/{frost-bls12-381-g1 => frost}/src/lib.rs (100%) rename crates/{frost-bls12-381-g1 => frost}/src/tests.rs (100%) rename crates/{frost-bls12-381-g1 => frost}/tests/kryptology_fixtures/2-of-3-ctx-0.json (100%) rename crates/{frost-bls12-381-g1 => frost}/tests/kryptology_fixtures/3-of-3-ctx-0.json (100%) rename crates/{frost-bls12-381-g1 => frost}/tests/kryptology_fixtures/invalid-proof.json (100%) rename crates/{frost-bls12-381-g1 => frost}/tests/kryptology_fixtures/malformed-share-id.json (100%) rename crates/{frost-bls12-381-g1 => frost}/tests/kryptology_interop.rs (100%) rename crates/{frost-bls12-381-g1 => frost}/tests/kryptology_round_trip.rs (100%) diff --git a/crates/frost-bls12-381-g1/Cargo.toml b/crates/frost/Cargo.toml similarity index 100% rename from crates/frost-bls12-381-g1/Cargo.toml rename to crates/frost/Cargo.toml diff --git a/crates/frost-bls12-381-g1/README.md b/crates/frost/README.md similarity index 100% rename from crates/frost-bls12-381-g1/README.md rename to crates/frost/README.md diff --git a/crates/frost-bls12-381-g1/dkg.md b/crates/frost/dkg.md similarity index 100% rename from crates/frost-bls12-381-g1/dkg.md rename to crates/frost/dkg.md diff --git a/crates/frost-bls12-381-g1/src/curve.rs b/crates/frost/src/curve.rs similarity index 100% rename from crates/frost-bls12-381-g1/src/curve.rs rename to crates/frost/src/curve.rs diff --git a/crates/frost-bls12-381-g1/src/frost_core.rs b/crates/frost/src/frost_core.rs similarity index 100% rename from crates/frost-bls12-381-g1/src/frost_core.rs rename to crates/frost/src/frost_core.rs diff --git a/crates/frost-bls12-381-g1/src/kryptology.rs b/crates/frost/src/kryptology.rs similarity index 100% rename from crates/frost-bls12-381-g1/src/kryptology.rs rename to crates/frost/src/kryptology.rs diff --git a/crates/frost-bls12-381-g1/src/lib.rs b/crates/frost/src/lib.rs similarity index 100% rename from crates/frost-bls12-381-g1/src/lib.rs rename to crates/frost/src/lib.rs diff --git a/crates/frost-bls12-381-g1/src/tests.rs b/crates/frost/src/tests.rs similarity index 100% rename from crates/frost-bls12-381-g1/src/tests.rs rename to crates/frost/src/tests.rs diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/2-of-3-ctx-0.json b/crates/frost/tests/kryptology_fixtures/2-of-3-ctx-0.json similarity index 100% rename from crates/frost-bls12-381-g1/tests/kryptology_fixtures/2-of-3-ctx-0.json rename to crates/frost/tests/kryptology_fixtures/2-of-3-ctx-0.json diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/3-of-3-ctx-0.json b/crates/frost/tests/kryptology_fixtures/3-of-3-ctx-0.json similarity index 100% rename from crates/frost-bls12-381-g1/tests/kryptology_fixtures/3-of-3-ctx-0.json rename to crates/frost/tests/kryptology_fixtures/3-of-3-ctx-0.json diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/invalid-proof.json b/crates/frost/tests/kryptology_fixtures/invalid-proof.json similarity index 100% rename from crates/frost-bls12-381-g1/tests/kryptology_fixtures/invalid-proof.json rename to crates/frost/tests/kryptology_fixtures/invalid-proof.json diff --git a/crates/frost-bls12-381-g1/tests/kryptology_fixtures/malformed-share-id.json b/crates/frost/tests/kryptology_fixtures/malformed-share-id.json similarity index 100% rename from crates/frost-bls12-381-g1/tests/kryptology_fixtures/malformed-share-id.json rename to crates/frost/tests/kryptology_fixtures/malformed-share-id.json diff --git a/crates/frost-bls12-381-g1/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs similarity index 100% rename from crates/frost-bls12-381-g1/tests/kryptology_interop.rs rename to crates/frost/tests/kryptology_interop.rs diff --git a/crates/frost-bls12-381-g1/tests/kryptology_round_trip.rs b/crates/frost/tests/kryptology_round_trip.rs similarity index 100% rename from crates/frost-bls12-381-g1/tests/kryptology_round_trip.rs rename to crates/frost/tests/kryptology_round_trip.rs From 1e03f39d1eee098125348cdb8e6020d80a5d6b1d Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:34:27 -0300 Subject: [PATCH 03/23] Add crate --- Cargo.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index f0a08095..23d7af85 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ "crates/testutil", "crates/tracing", "crates/peerinfo", + "crates/frost", ] resolver = "3" @@ -115,6 +116,7 @@ pluto-testutil = { path = "crates/testutil" } pluto-tracing = { path = "crates/tracing" } pluto-p2p = { path = "crates/p2p" } pluto-peerinfo = { path = "crates/peerinfo" } +pluto-frost = { path = "crates/frost" } [workspace.lints.rust] missing_docs = "deny" From cbfb2ee77b25f2031cd5eaca2793b62724220e17 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:35:47 -0300 Subject: [PATCH 04/23] Fix crate toml --- Cargo.lock | 12 ++++++++++++ crates/frost/Cargo.toml | 19 ++++++------------- 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b92c97f7..6a113a24 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5669,6 +5669,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "pluto-frost" +version = "1.7.1" +dependencies = [ + "blst", + "hex", + "rand 0.8.5", + "rand_core 0.6.4", + "serde_json", + "sha2", +] + [[package]] name = "pluto-k1util" version = "1.7.1" diff --git a/crates/frost/Cargo.toml b/crates/frost/Cargo.toml index 8ca8c1dd..c589335b 100644 --- a/crates/frost/Cargo.toml +++ b/crates/frost/Cargo.toml @@ -1,26 +1,19 @@ [package] -name = "frost-bls12-381-g1" -edition.workspace = true -rust-version.workspace = true +name = "pluto-frost" version.workspace = true -authors.workspace = true -readme = "README.md" -license.workspace = true +edition.workspace = true repository.workspace = true -categories.workspace = true -keywords = ["cryptography", "crypto", "bls12-381", "threshold", "signature"] -description = "Kryptology-compatible FROST DKG and BLS threshold signing over BLS12-381 G1." +license.workspace = true +publish.workspace = true [dependencies] -blst = { version = "0.3", default-features = false } +blst.workspace = true rand_core.workspace = true -sha2 = { version = "0.10.2", default-features = false } +sha2.workspace = true [dev-dependencies] hex.workspace = true -lazy_static.workspace = true rand.workspace = true -rand_chacha.workspace = true serde_json.workspace = true [lib] From f1166951e1d9f2981dda8f948b152c77fb9d5d2d Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:37:59 -0300 Subject: [PATCH 05/23] Fix imports and compiler issues - Adjust `&` handling in patterns --- crates/frost/src/tests.rs | 17 ++++++++--------- crates/frost/tests/kryptology_interop.rs | 2 +- crates/frost/tests/kryptology_round_trip.rs | 4 ++-- 3 files changed, 11 insertions(+), 12 deletions(-) diff --git a/crates/frost/src/tests.rs b/crates/frost/src/tests.rs index 08934fd7..1c1bf190 100644 --- a/crates/frost/src/tests.rs +++ b/crates/frost/src/tests.rs @@ -1,8 +1,6 @@ -use alloc::collections::BTreeMap; -use alloc::vec::Vec; +use alloc::{collections::BTreeMap, vec::Vec}; -use rand::rngs::StdRng; -use rand::SeedableRng; +use rand::{SeedableRng, rngs::StdRng}; use crate::kryptology::{self, BlsPartialSignature}; @@ -113,8 +111,8 @@ fn check_kryptology_bls_round_trip_3_of_3() { // Collect broadcasts from everyone except ourselves let received_bcasts: BTreeMap = bcasts .iter() - .filter(|(&k, _)| k != id) - .map(|(&k, v)| (k, v.clone())) + .filter(|(k, _)| **k != id) + .map(|(k, v)| (*k, v.clone())) .collect(); let received_shares = all_shares.remove(&id).unwrap(); @@ -198,8 +196,8 @@ fn check_kryptology_bls_round_trip_2_of_3() { for id in 1..=max_signers as u32 { let received_bcasts: BTreeMap<_, _> = bcasts .iter() - .filter(|(&k, _)| k != id) - .map(|(&k, v)| (k, v.clone())) + .filter(|(k, _)| **k != id) + .map(|(k, v)| (*k, v.clone())) .collect(); let received_shares = all_shares.remove(&id).unwrap(); let secret = secrets.remove(&id).unwrap(); @@ -269,7 +267,8 @@ fn check_kryptology_invalid_proof_rejected() { } } -/// Verify that a share addressed to the wrong participant is rejected in round2. +/// Verify that a share addressed to the wrong participant is rejected in +/// round2. #[test] fn check_kryptology_share_id_mismatch_rejected() { let mut rng = StdRng::seed_from_u64(42); diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs index f1caf547..e6c349bd 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/tests/kryptology_interop.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; -use frost_bls12_381_g1::kryptology; +use pluto_frost::kryptology; use serde_json::Value; const FIXTURE_2_OF_3_CTX_0: &str = include_str!("./kryptology_fixtures/2-of-3-ctx-0.json"); diff --git a/crates/frost/tests/kryptology_round_trip.rs b/crates/frost/tests/kryptology_round_trip.rs index 46037d17..210ea62a 100644 --- a/crates/frost/tests/kryptology_round_trip.rs +++ b/crates/frost/tests/kryptology_round_trip.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; -use frost_bls12_381_g1::kryptology; -use rand::{rngs::StdRng, SeedableRng}; +use pluto_frost::kryptology; +use rand::{SeedableRng, rngs::StdRng}; /// Follows: https://github.com/coinbase/kryptology/blob/f129811df480d7c65115a20b0461c4e406bd28f2/test/frost_dkg/bls/main.go#L23 /// From 9b989ea3ebb79b46ffc3a6e0015dc7d80cd06638 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:38:35 -0300 Subject: [PATCH 06/23] Apply formatting --- crates/frost/src/curve.rs | 34 +++++++++++++++++++++++----------- crates/frost/src/frost_core.rs | 11 +++++++---- crates/frost/src/kryptology.rs | 13 +++++++------ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/crates/frost/src/curve.rs b/crates/frost/src/curve.rs index 088caf41..4607f000 100644 --- a/crates/frost/src/curve.rs +++ b/crates/frost/src/curve.rs @@ -1,10 +1,14 @@ -//! Thin wrappers around [`blst`] types for the BLS12-381 scalar field and G1 curve group. +//! Thin wrappers around [`blst`] types for the BLS12-381 scalar field and G1 +//! curve group. //! -//! Provides [`Scalar`], [`G1Projective`], and [`G1Affine`] with arithmetic operator -//! overloads, serialization, and safe constructors that enforce subgroup membership. +//! Provides [`Scalar`], [`G1Projective`], and [`G1Affine`] with arithmetic +//! operator overloads, serialization, and safe constructors that enforce +//! subgroup membership. -use core::fmt; -use core::ops::{Add, Mul, Sub}; +use core::{ + fmt, + ops::{Add, Mul, Sub}, +}; use blst::*; use rand_core::{CryptoRng, RngCore}; @@ -20,9 +24,6 @@ impl fmt::Debug for Scalar { } impl Scalar { - /// Additive identity. - pub const ZERO: Self = Scalar(blst_fr { l: [0; 4] }); - /// Multiplicative identity. pub const ONE: Self = { // Montgomery form of 1 for BLS12-381 scalar field. @@ -38,6 +39,8 @@ impl Scalar { ], }) }; + /// Additive identity. + pub const ZERO: Self = Scalar(blst_fr { l: [0; 4] }); /// Serialize to 32 little-endian bytes. pub fn to_bytes(&self) -> [u8; 32] { @@ -104,6 +107,7 @@ impl From for Scalar { impl Add for Scalar { type Output = Self; + fn add(self, rhs: Self) -> Self { let mut out = blst_fr::default(); unsafe { blst_fr_add(&mut out, &self.0, &rhs.0) }; @@ -113,6 +117,7 @@ impl Add for Scalar { impl Sub for Scalar { type Output = Self; + fn sub(self, rhs: Self) -> Self { let mut out = blst_fr::default(); unsafe { blst_fr_sub(&mut out, &self.0, &rhs.0) }; @@ -122,6 +127,7 @@ impl Sub for Scalar { impl Mul for Scalar { type Output = Self; + fn mul(self, rhs: Self) -> Self { let mut out = blst_fr::default(); unsafe { blst_fr_mul(&mut out, &self.0, &rhs.0) }; @@ -129,7 +135,8 @@ impl Mul for Scalar { } } -/// BLS12-381 G1 point in projective (Jacobian) coordinates. Wrapper around `blst_p1`. +/// BLS12-381 G1 point in projective (Jacobian) coordinates. Wrapper around +/// `blst_p1`. #[derive(Copy, Clone, Default, Eq)] pub struct G1Projective(pub(crate) blst_p1); @@ -164,7 +171,8 @@ impl G1Projective { } /// Deserialize from 48-byte compressed form. - /// Returns `None` on invalid encoding or point not in G1, or the identity (point at infinity). + /// Returns `None` on invalid encoding or point not in G1, or the identity + /// (point at infinity). pub fn from_compressed(bytes: &[u8; 48]) -> Option { let affine = G1Affine::from_compressed(bytes)?; if affine.is_identity() { @@ -176,6 +184,7 @@ impl G1Projective { impl Add for G1Projective { type Output = Self; + fn add(self, rhs: Self) -> Self { let mut out = blst_p1::default(); unsafe { blst_p1_add_or_double(&mut out, &self.0, &rhs.0) }; @@ -185,6 +194,7 @@ impl Add for G1Projective { impl Sub for G1Projective { type Output = Self; + fn sub(self, rhs: Self) -> Self { let mut neg = rhs.0; let mut out = blst_p1::default(); @@ -198,6 +208,7 @@ impl Sub for G1Projective { impl Mul for G1Projective { type Output = Self; + fn mul(self, rhs: Scalar) -> Self { let mut scalar = blst_scalar::default(); let mut out = blst_p1::default(); @@ -209,7 +220,8 @@ impl Mul for G1Projective { } } -/// BLS12-381 G1 point in affine coordinates (for serialization). Wrapper around `blst_p1_affine`. +/// BLS12-381 G1 point in affine coordinates (for serialization). Wrapper around +/// `blst_p1_affine`. #[derive(Copy, Clone, Default)] pub struct G1Affine(pub(crate) blst_p1_affine); diff --git a/crates/frost/src/frost_core.rs b/crates/frost/src/frost_core.rs index c15dbcf0..60d534f8 100644 --- a/crates/frost/src/frost_core.rs +++ b/crates/frost/src/frost_core.rs @@ -1,11 +1,14 @@ -//! Port of frost-core types and functions, specialized for BLS12-381 G1 curve operations. +//! Port of frost-core types and functions, specialized for BLS12-381 G1 curve +//! operations. //! //! Contains the key material types (identifiers, shares, packages) and the //! polynomial evaluation functions needed by the kryptology-compatible DKG. -use alloc::collections::{BTreeMap, BTreeSet}; -use alloc::vec; -use alloc::vec::Vec; +use alloc::{ + collections::{BTreeMap, BTreeSet}, + vec, + vec::Vec, +}; use core::cmp::Ordering; use super::*; diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index 4d479288..124bbb42 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -9,8 +9,7 @@ //! The output types ([`KeyPackage`], [`PublicKeyPackage`]) are standard //! frost-core types usable with frost-core's signing protocol. -use alloc::collections::BTreeMap; -use alloc::{vec, vec::Vec}; +use alloc::{collections::BTreeMap, vec, vec::Vec}; use blst::*; use rand_core::{CryptoRng, RngCore}; @@ -354,7 +353,8 @@ pub fn round1( /// /// # Arguments /// - `secret`: The [`Round1Secret`] from this participant's [`round1`] call. -/// - `received_bcasts`: Map from source participant ID to their [`Round1Bcast`]. +/// - `received_bcasts`: Map from source participant ID to their +/// [`Round1Bcast`]. /// - `received_shares`: Map from source participant ID to the [`ShamirShare`] /// they sent us. pub fn round2( @@ -445,7 +445,8 @@ pub fn round2( Ok((bcast, key_package, public_key_package)) } -/// Domain separation tag for Ethereum 2.0 BLS signatures (proof of possession scheme). +/// Domain separation tag for Ethereum 2.0 BLS signatures (proof of possession +/// scheme). /// /// Matches Go's `bls.NewSigEth2()` which uses `blsSignaturePopDst`. pub const BLS_SIG_DST: &[u8] = b"BLS_SIG_BLS12381G2_XMD:SHA-256_SSWU_RO_POP_"; @@ -462,8 +463,8 @@ impl BlsPartialSignature { /// Produce a BLS partial signature from a [`KeyPackage`] produced by /// kryptology DKG. /// - /// Computes `partial_sig = (key_package.signing_share) * H(msg)` where H hashes the message - /// to a G2 point using the Ethereum 2.0 DST. + /// Computes `partial_sig = (key_package.signing_share) * H(msg)` where H + /// hashes the message to a G2 point using the Ethereum 2.0 DST. /// /// The `id` must be the original 1-indexed kryptology participant ID. pub fn from_key_package(id: u32, key_package: &KeyPackage, msg: &[u8]) -> BlsPartialSignature { From 5aa73eb9df8eb2c02a530d26b340ed5944733e95 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:39:18 -0300 Subject: [PATCH 07/23] Apply clippy suggestions --- crates/frost/src/tests.rs | 6 +++--- crates/frost/tests/kryptology_interop.rs | 2 +- crates/frost/tests/kryptology_round_trip.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/frost/src/tests.rs b/crates/frost/src/tests.rs index 1c1bf190..67e7bcf3 100644 --- a/crates/frost/src/tests.rs +++ b/crates/frost/src/tests.rs @@ -158,7 +158,7 @@ fn check_kryptology_bls_round_trip_3_of_3() { .expect("BLS signature combination should succeed"); assert!( - signature.verify(&vk, message), + signature.verify(vk, message), "3-of-3 BLS threshold signature should verify" ); } @@ -224,13 +224,13 @@ fn check_kryptology_bls_round_trip_2_of_3() { let vk = public_key_packages[0].verifying_key(); assert!( - signature.verify(&vk, message), + signature.verify(vk, message), "BLS threshold signature should verify" ); // Verify wrong message fails assert!( - !signature.verify(&vk, b"wrong message"), + !signature.verify(vk, b"wrong message"), "BLS signature should not verify against a different message" ); } diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs index e6c349bd..5cf68a69 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/tests/kryptology_interop.rs @@ -144,7 +144,7 @@ fn replay_fixture(json: &str, require_group_signature: bool) { .expect("BLS signature combination should succeed"); assert!( - signature.verify(&vk, message), + signature.verify(vk, message), "fixture-derived BLS threshold signature should verify" ); } diff --git a/crates/frost/tests/kryptology_round_trip.rs b/crates/frost/tests/kryptology_round_trip.rs index 210ea62a..2c3721d3 100644 --- a/crates/frost/tests/kryptology_round_trip.rs +++ b/crates/frost/tests/kryptology_round_trip.rs @@ -105,7 +105,7 @@ fn check_kryptology_bls_round_trip_2_of_4_ctx_0() { .expect("BLS signature combination should succeed"); assert!( - signature.verify(&group_key, message), + signature.verify(group_key, message), "BLS threshold signature should verify against the group public key" ); } From c58b00a5abc5755378a490b66a455acd3185d2b7 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:41:50 -0300 Subject: [PATCH 08/23] Adjust docs --- crates/frost/dkg.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/frost/dkg.md b/crates/frost/dkg.md index 0e0c3ba4..97bd74bf 100644 --- a/crates/frost/dkg.md +++ b/crates/frost/dkg.md @@ -24,7 +24,7 @@ Gob encoding is not part of this interoperability contract. ```rust use std::collections::BTreeMap; -use frost_bls12_381_g1::kryptology; +use pluto_frost::kryptology; let mut rng = rand::rngs::OsRng; @@ -33,9 +33,9 @@ let max_signers = 5u16; let ctx = 0u8; // Round 1: each participant generates broadcast data and shares. -let mut bcasts = BTreeMap::new(); +let mut bcasts: BTreeMap = BTreeMap::new(); let mut all_shares: BTreeMap> = BTreeMap::new(); -let mut secrets = BTreeMap::new(); +let mut secrets: BTreeMap = BTreeMap::new(); for id in 1..=max_signers as u32 { let (bcast, shares, secret) = @@ -55,8 +55,8 @@ let mut public_key_packages = Vec::new(); for id in 1..=max_signers as u32 { let received_bcasts: BTreeMap<_, _> = bcasts .iter() - .filter(|(&k, _)| k != id) - .map(|(&k, v)| (k, v.clone())) + .filter(|(k, _)| **k != id) + .map(|(k, v)| (*k, v.clone())) .collect(); let received_shares = all_shares.remove(&id).unwrap(); let secret = secrets.remove(&id).unwrap(); From 0569fcd18f155e7afe061e89d34725d66d2357f6 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:43:52 -0300 Subject: [PATCH 09/23] Inline readme --- crates/frost/README.md | 7 ------- crates/frost/src/lib.rs | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) delete mode 100644 crates/frost/README.md diff --git a/crates/frost/README.md b/crates/frost/README.md deleted file mode 100644 index 0c9f38fd..00000000 --- a/crates/frost/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# frost-bls12-381-g1 - -Kryptology-compatible FROST DKG and BLS threshold signing over BLS12-381 G1. - -This crate implements a distributed key generation protocol compatible with -Go's Coinbase Kryptology FROST DKG, and BLS threshold signing (Ethereum 2.0 -compatible). diff --git a/crates/frost/src/lib.rs b/crates/frost/src/lib.rs index 04825dcb..9885e8c0 100644 --- a/crates/frost/src/lib.rs +++ b/crates/frost/src/lib.rs @@ -1,3 +1,8 @@ +//! Kryptology-compatible FROST DKG and BLS threshold signing over BLS12-381 G1. +//! This crate implements a distributed key generation protocol compatible with +//! Go's Coinbase Kryptology FROST DKG, and BLS threshold signing (Ethereum 2.0 +//! compatible). + #![no_std] #![allow(non_snake_case)] #![deny(missing_docs)] From 68104365935b17c1201534c711de28957680b9f7 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:45:11 -0300 Subject: [PATCH 10/23] Remove bench-specific configs --- crates/frost/Cargo.toml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/crates/frost/Cargo.toml b/crates/frost/Cargo.toml index c589335b..5fffcc41 100644 --- a/crates/frost/Cargo.toml +++ b/crates/frost/Cargo.toml @@ -15,8 +15,3 @@ sha2.workspace = true hex.workspace = true rand.workspace = true serde_json.workspace = true - -[lib] -# Disables non-criterion benchmark which is not used; prevents errors -# when using criterion-specific flags -bench = false From 944a6bc6c64413627da07decbe97f48aec0bb3c5 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 12:46:10 -0300 Subject: [PATCH 11/23] Reduce attribute usage --- crates/frost/src/lib.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/frost/src/lib.rs b/crates/frost/src/lib.rs index 9885e8c0..c6a0d48d 100644 --- a/crates/frost/src/lib.rs +++ b/crates/frost/src/lib.rs @@ -3,11 +3,7 @@ //! Go's Coinbase Kryptology FROST DKG, and BLS threshold signing (Ethereum 2.0 //! compatible). -#![no_std] #![allow(non_snake_case)] -#![deny(missing_docs)] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] -#![cfg_attr(docsrs, feature(doc_cfg))] #![doc = include_str!("../dkg.md")] extern crate alloc; From 8de0c79faaea287e85b0d75318d4a9597037de9e Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 13:05:07 -0300 Subject: [PATCH 12/23] Use std imports --- crates/frost/src/curve.rs | 2 +- crates/frost/src/frost_core.rs | 6 ++---- crates/frost/src/kryptology.rs | 4 ++-- crates/frost/src/lib.rs | 2 -- crates/frost/src/tests.rs | 8 +++++--- 5 files changed, 10 insertions(+), 12 deletions(-) diff --git a/crates/frost/src/curve.rs b/crates/frost/src/curve.rs index 4607f000..70ebd413 100644 --- a/crates/frost/src/curve.rs +++ b/crates/frost/src/curve.rs @@ -5,7 +5,7 @@ //! operator overloads, serialization, and safe constructors that enforce //! subgroup membership. -use core::{ +use std::{ fmt, ops::{Add, Mul, Sub}, }; diff --git a/crates/frost/src/frost_core.rs b/crates/frost/src/frost_core.rs index 60d534f8..99681f3a 100644 --- a/crates/frost/src/frost_core.rs +++ b/crates/frost/src/frost_core.rs @@ -4,12 +4,10 @@ //! Contains the key material types (identifiers, shares, packages) and the //! polynomial evaluation functions needed by the kryptology-compatible DKG. -use alloc::{ +use std::{ + cmp::Ordering, collections::{BTreeMap, BTreeSet}, - vec, - vec::Vec, }; -use core::cmp::Ordering; use super::*; diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index 124bbb42..166c7db3 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -9,7 +9,7 @@ //! The output types ([`KeyPackage`], [`PublicKeyPackage`]) are standard //! frost-core types usable with frost-core's signing protocol. -use alloc::{collections::BTreeMap, vec, vec::Vec}; +use std::collections::BTreeMap; use blst::*; use rand_core::{CryptoRng, RngCore}; @@ -514,7 +514,7 @@ impl BlsSignature { } // Check for duplicate identifiers - let mut seen = alloc::collections::BTreeSet::new(); + let mut seen = std::collections::BTreeSet::new(); for ps in partial_sigs { if !seen.insert(ps.identifier) { return Err(DkgError::DuplicateIdentifier(ps.identifier)); diff --git a/crates/frost/src/lib.rs b/crates/frost/src/lib.rs index c6a0d48d..1197bf98 100644 --- a/crates/frost/src/lib.rs +++ b/crates/frost/src/lib.rs @@ -6,8 +6,6 @@ #![allow(non_snake_case)] #![doc = include_str!("../dkg.md")] -extern crate alloc; - pub mod curve; pub mod frost_core; pub mod kryptology; diff --git a/crates/frost/src/tests.rs b/crates/frost/src/tests.rs index 67e7bcf3..92715aa8 100644 --- a/crates/frost/src/tests.rs +++ b/crates/frost/src/tests.rs @@ -1,8 +1,8 @@ -use alloc::{collections::BTreeMap, vec::Vec}; +use std::collections::BTreeMap; use rand::{SeedableRng, rngs::StdRng}; -use crate::kryptology::{self, BlsPartialSignature}; +use crate::kryptology; #[test] fn check_scalar_one_precomputed() { @@ -151,7 +151,9 @@ fn check_kryptology_bls_round_trip_3_of_3() { let partial_sigs: Vec<_> = key_packages .keys() - .map(|&id| BlsPartialSignature::from_key_package(id, &key_packages[&id], message)) + .map(|&id| { + kryptology::BlsPartialSignature::from_key_package(id, &key_packages[&id], message) + }) .collect(); let signature = kryptology::BlsSignature::from_partial_signatures(threshold, &partial_sigs) From bc49b223f8e9f417d128d5b6c249c594e81df73f Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 17:51:10 -0300 Subject: [PATCH 13/23] Add proper permalinks --- crates/frost/src/frost_core.rs | 54 ++++++++------------- crates/frost/src/kryptology.rs | 3 +- crates/frost/tests/kryptology_round_trip.rs | 4 +- 3 files changed, 24 insertions(+), 37 deletions(-) diff --git a/crates/frost/src/frost_core.rs b/crates/frost/src/frost_core.rs index 99681f3a..2354ffda 100644 --- a/crates/frost/src/frost_core.rs +++ b/crates/frost/src/frost_core.rs @@ -30,7 +30,7 @@ pub enum FrostCoreError { /// A participant identifier wrapping a non-zero scalar. /// -/// Ported from frost-core/src/identifier.rs:26-48 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/identifier.rs#L14-L26 #[derive(Copy, Clone, Debug)] pub struct Identifier(Scalar); @@ -65,13 +65,13 @@ impl PartialOrd for Identifier { } } +// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/identifier.rs#L121-L137 impl Ord for Identifier { + /// Compare identifiers by their numeric scalar value, using big-endian byte + /// order. Serializes to little-endian, and compares in reverse order. fn cmp(&self, other: &Self) -> Ordering { - // Compare using serialized bytes in little-endian order. - // Ported from frost-core/src/identifier.rs:131-146 let a = self.0.to_bytes(); let b = other.0.to_bytes(); - // Compare most-significant byte first (reversed from LE storage). for i in (0..32).rev() { match a[i].cmp(&b[i]) { Ordering::Equal => continue, @@ -84,7 +84,7 @@ impl Ord for Identifier { /// A commitment to a single polynomial coefficient (a group element). /// -/// Ported from frost-core/src/keys.rs:249-274 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L242-L249 #[derive(Copy, Clone, Debug)] pub struct CoefficientCommitment(G1Projective); @@ -103,7 +103,7 @@ impl CoefficientCommitment { /// The commitments to the coefficients of a secret polynomial, used for /// Feldman verifiable secret sharing. /// -/// Ported from frost-core/src/keys.rs:308-382 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L293-L310 #[derive(Clone, Debug)] pub struct VerifiableSecretSharingCommitment(Vec); @@ -131,28 +131,22 @@ impl VerifiableSecretSharingCommitment { /// A secret scalar value representing a signer's share of the group secret. /// -/// Ported from frost-core/src/keys.rs:87-121 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L82-L87 #[derive(Copy, Clone, Debug)] pub struct SigningShare(Scalar); impl SigningShare { /// Create a signing share from a scalar. - /// - /// Ported from frost-core/src/keys.rs:96-98 pub fn new(scalar: Scalar) -> Self { Self(scalar) } /// Return the underlying scalar. - /// - /// Ported from frost-core/src/keys.rs:103-105 pub fn to_scalar(&self) -> Scalar { self.0 } /// Evaluate the polynomial defined by `coefficients` at `peer`. - /// - /// Ported from frost-core/src/keys.rs:119-121 pub fn from_coefficients(coefficients: &[Scalar], peer: Identifier) -> Self { Self::new(evaluate_polynomial(peer, coefficients)) } @@ -160,7 +154,7 @@ impl SigningShare { /// A public group element that represents a single signer's public /// verification share. /// -/// Ported from frost-core/src/keys.rs:163-214 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L158-L165 #[derive(Copy, Clone, Debug)] pub struct VerifyingShare(G1Projective); @@ -177,8 +171,6 @@ impl VerifyingShare { /// Compute the verifying share for `identifier` from the summed VSS /// commitment. - /// - /// Ported from frost-core/src/keys.rs:198-214 pub fn from_commitment( identifier: Identifier, commitment: &VerifiableSecretSharingCommitment, @@ -189,7 +181,7 @@ impl VerifyingShare { /// The group public key, used to verify threshold signatures. /// -/// Ported from frost-core/src/verifying_key.rs:15-93 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/verifying_key.rs#L10-L20 #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub struct VerifyingKey(G1Projective); @@ -206,7 +198,7 @@ impl VerifyingKey { /// Derive the verifying key from the first coefficient commitment. /// - /// Ported from frost-core/src/verifying_key.rs:83-93 + /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/verifying_key.rs#L81-L93 pub fn from_commitment( commitment: &VerifiableSecretSharingCommitment, ) -> Result { @@ -222,7 +214,7 @@ impl VerifyingKey { /// Secret and public key material generated during DKG. /// -/// Ported from frost-core/src/keys.rs:399-468 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L384-L411 pub struct SecretShare { identifier: Identifier, signing_share: SigningShare, @@ -231,8 +223,6 @@ pub struct SecretShare { impl SecretShare { /// Create a new secret share. - /// - /// Ported from frost-core/src/keys.rs:418-429 pub fn new( identifier: Identifier, signing_share: SigningShare, @@ -249,7 +239,7 @@ impl SecretShare { /// /// Checks that `G * signing_share == evaluate_vss(identifier, commitment)`. /// - /// Ported from frost-core/src/keys.rs:445-468 + /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L431-L468 pub fn verify(&self) -> Result<(), FrostCoreError> { let f_result = G1Projective::generator() * self.signing_share.to_scalar(); let result = evaluate_vss(self.identifier, &self.commitment); @@ -264,7 +254,7 @@ impl SecretShare { /// A key package containing all key material for a participant. /// -/// Ported from frost-core/src/keys.rs:627-665 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L617-L643 #[derive(Debug)] pub struct KeyPackage { identifier: Identifier, @@ -276,8 +266,6 @@ pub struct KeyPackage { impl KeyPackage { /// Create a new key package. - /// - /// Ported from frost-core/src/keys.rs:650-665 pub fn new( identifier: Identifier, signing_share: SigningShare, @@ -323,7 +311,7 @@ impl KeyPackage { /// Public data containing all signers' verification shares and the group /// public key. /// -/// Ported from frost-core/src/keys.rs:720-777 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L712-L729 #[derive(Debug)] pub struct PublicKeyPackage { verifying_shares: BTreeMap, @@ -332,8 +320,6 @@ pub struct PublicKeyPackage { impl PublicKeyPackage { /// Create a new public key package. - /// - /// Ported from frost-core/src/keys.rs:736-745 pub fn new( verifying_shares: BTreeMap, verifying_key: VerifyingKey, @@ -356,7 +342,7 @@ impl PublicKeyPackage { /// Derive a public key package from all participants' DKG commitments. /// - /// Ported from frost-core/src/keys.rs:770-777 + /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L765-L777 pub fn from_dkg_commitments( commitments: &BTreeMap, ) -> Result { @@ -368,7 +354,7 @@ impl PublicKeyPackage { /// Derive verifying shares for each participant from a summed commitment. /// - /// Ported from frost-core/src/keys.rs:751-763 + /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L747-L763 fn from_commitment( identifiers: &BTreeSet, commitment: &VerifiableSecretSharingCommitment, @@ -389,7 +375,7 @@ impl PublicKeyPackage { /// Given coefficients `[a_0, a_1, ..., a_{t-1}]`, computes /// `a_0 + a_1 * x + a_2 * x^2 + ... + a_{t-1} * x^{t-1}`. /// -/// Ported from frost-core/src/keys.rs:579-595 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L573-L595 fn evaluate_polynomial(identifier: Identifier, coefficients: &[Scalar]) -> Scalar { let mut value = Scalar::ZERO; let x = identifier.to_scalar(); @@ -409,7 +395,7 @@ fn evaluate_polynomial(identifier: Identifier, coefficients: &[Scalar]) -> Scala /// /// Computes `sum_{k=0}^{t-1} commitment[k] * identifier^k`. /// -/// Ported from frost-core/src/keys.rs:602-615 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L597-L615 fn evaluate_vss( identifier: Identifier, commitment: &VerifiableSecretSharingCommitment, @@ -431,7 +417,7 @@ fn evaluate_vss( /// commitment of length t where each element is the sum of the corresponding /// elements across all participants. /// -/// Ported from frost-core/src/keys.rs:38-62 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L35-L62 fn sum_commitments( commitments: &[&VerifiableSecretSharingCommitment], ) -> Result { @@ -460,7 +446,7 @@ fn sum_commitments( /// Validate that (min_signers, max_signers) form a valid pair. /// -/// Ported from frost-core/src/keys.rs:798-815 +/// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L796-L815 pub fn validate_num_of_signers(min_signers: u16, max_signers: u16) -> Result<(), FrostCoreError> { if min_signers < 2 { return Err(FrostCoreError::InvalidMinSigners); diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index 166c7db3..26454ba6 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -207,7 +207,8 @@ pub fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { } /// Kryptology hash-to-scalar. -/// See: https://github.com/coinbase/kryptology/blob/eef703320df46f97e86ead4eff178b095181b0ec/pkg/core/curves/bls12381_curve.go#L50 +/// +/// See: https://github.com/coinbase/kryptology/blob/1dcc062313d99f2e56ce6abc2003ef63c52dd4a5/pkg/core/curves/bls12381_curve.go#L50 const KRYPTOLOGY_DST: &[u8] = b"BLS12381_XMD:SHA-256_SSWU_RO_"; /// Hash to scalar using kryptology's ExpandMsgXmd construction. diff --git a/crates/frost/tests/kryptology_round_trip.rs b/crates/frost/tests/kryptology_round_trip.rs index 2c3721d3..85625e32 100644 --- a/crates/frost/tests/kryptology_round_trip.rs +++ b/crates/frost/tests/kryptology_round_trip.rs @@ -3,11 +3,11 @@ use std::collections::BTreeMap; use pluto_frost::kryptology; use rand::{SeedableRng, rngs::StdRng}; -/// Follows: https://github.com/coinbase/kryptology/blob/f129811df480d7c65115a20b0461c4e406bd28f2/test/frost_dkg/bls/main.go#L23 -/// /// FROST DKG + BLS threshold signing (Ethereum 2.0 compatible). /// This matches Go's signing flow: non-interactive BLS partial signatures /// combined via Lagrange interpolation, verified with standard BLS pairings. +/// +/// See: https://github.com/coinbase/kryptology/blob/1dcc062313d99f2e56ce6abc2003ef63c52dd4a5/test/frost_dkg/bls/main.go#L23 #[test] fn check_kryptology_bls_round_trip_2_of_4_ctx_0() { let mut rng = StdRng::seed_from_u64(20260410); From 9b0b81bce78a3a42493a97f6a875ac54597b3ee0 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 17:56:31 -0300 Subject: [PATCH 14/23] Adjust test names --- crates/frost/src/tests.rs | 20 ++++++++++---------- crates/frost/tests/kryptology_interop.rs | 8 ++++---- crates/frost/tests/kryptology_round_trip.rs | 2 +- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/crates/frost/src/tests.rs b/crates/frost/src/tests.rs index 92715aa8..64f913df 100644 --- a/crates/frost/src/tests.rs +++ b/crates/frost/src/tests.rs @@ -5,7 +5,7 @@ use rand::{SeedableRng, rngs::StdRng}; use crate::kryptology; #[test] -fn check_scalar_one_precomputed() { +fn scalar_one_precomputed() { let constant = crate::Scalar::ONE; let computed = crate::Scalar::from(1u64); assert_eq!(constant, computed); @@ -15,7 +15,7 @@ fn check_scalar_one_precomputed() { /// DST = "QUUX-V01-CS02-with-expander-SHA256-128" /// msg = "" (empty), len_in_bytes = 0x20 (32) #[test] -fn check_expand_msg_xmd_rfc9380_vector() { +fn expand_msg_xmd_rfc9380_vector() { let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; let msg = b""; let expected = @@ -27,7 +27,7 @@ fn check_expand_msg_xmd_rfc9380_vector() { /// RFC 9380 test vector: msg = "abc", len = 32 #[test] -fn check_expand_msg_xmd_rfc9380_abc() { +fn expand_msg_xmd_rfc9380_abc() { let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; let msg = b"abc"; let expected = @@ -39,7 +39,7 @@ fn check_expand_msg_xmd_rfc9380_abc() { /// RFC 9380 test vector: msg = "", len = 0x80 (128 bytes) #[test] -fn check_expand_msg_xmd_rfc9380_long_output() { +fn expand_msg_xmd_rfc9380_long_output() { let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; let msg = b""; let expected = hex::decode( @@ -56,7 +56,7 @@ fn check_expand_msg_xmd_rfc9380_long_output() { } #[test] -fn check_kryptology_rejects_more_than_255_signers() { +fn kryptology_rejects_more_than_255_signers() { let mut rng = StdRng::seed_from_u64(42); let result = kryptology::round1(1, 2, 256, 0, &mut rng); @@ -67,7 +67,7 @@ fn check_kryptology_rejects_more_than_255_signers() { } #[test] -fn check_kryptology_accepts_255_signers_boundary() { +fn kryptology_accepts_255_signers_boundary() { let mut rng = StdRng::seed_from_u64(4242); let (_bcast, shares, _secret) = kryptology::round1(1, 2, 255, 9, &mut rng) .expect("255 signers should remain within kryptology's u8 transport limit"); @@ -78,7 +78,7 @@ fn check_kryptology_accepts_255_signers_boundary() { /// Full DKG round-trip: 3-of-3 DKG, then BLS threshold sign and verify. #[test] -fn check_kryptology_bls_round_trip_3_of_3() { +fn kryptology_bls_round_trip_3_of_3() { let mut rng = StdRng::seed_from_u64(42); let threshold = 3u16; let max_signers = 3u16; @@ -167,7 +167,7 @@ fn check_kryptology_bls_round_trip_3_of_3() { /// 2-of-3 DKG then BLS threshold signing (Ethereum 2.0 compatible). #[test] -fn check_kryptology_bls_round_trip_2_of_3() { +fn kryptology_bls_round_trip_2_of_3() { let mut rng = StdRng::seed_from_u64(123); let threshold = 2u16; let max_signers = 3u16; @@ -239,7 +239,7 @@ fn check_kryptology_bls_round_trip_2_of_3() { /// Verify that an invalid proof is caught in round2. #[test] -fn check_kryptology_invalid_proof_rejected() { +fn kryptology_invalid_proof_rejected() { let mut rng = StdRng::seed_from_u64(99); let threshold = 2u16; let max_signers = 3u16; @@ -272,7 +272,7 @@ fn check_kryptology_invalid_proof_rejected() { /// Verify that a share addressed to the wrong participant is rejected in /// round2. #[test] -fn check_kryptology_share_id_mismatch_rejected() { +fn kryptology_share_id_mismatch_rejected() { let mut rng = StdRng::seed_from_u64(42); let threshold = 2u16; let max_signers = 3u16; diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs index 5cf68a69..45b4b72e 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/tests/kryptology_interop.rs @@ -39,22 +39,22 @@ struct FixtureScenario { } #[test] -fn check_kryptology_fixture_round2_interop_2_of_3_ctx_0() { +fn kryptology_fixture_round2_interop_2_of_3_ctx_0() { replay_fixture(FIXTURE_2_OF_3_CTX_0, true); } #[test] -fn check_kryptology_fixture_round2_interop_3_of_3_ctx_0() { +fn kryptology_fixture_round2_interop_3_of_3_ctx_0() { replay_fixture(FIXTURE_3_OF_3_CTX_0, true); } #[test] -fn check_kryptology_fixture_round2_interop_malformed_share_id() { +fn kryptology_fixture_round2_interop_malformed_share_id() { replay_fixture(FIXTURE_MALFORMED_SHARE_ID, false); } #[test] -fn check_kryptology_fixture_round2_interop_invalid_proof() { +fn kryptology_fixture_round2_interop_invalid_proof() { replay_fixture(FIXTURE_INVALID_PROOF, false); } diff --git a/crates/frost/tests/kryptology_round_trip.rs b/crates/frost/tests/kryptology_round_trip.rs index 85625e32..e4752aeb 100644 --- a/crates/frost/tests/kryptology_round_trip.rs +++ b/crates/frost/tests/kryptology_round_trip.rs @@ -9,7 +9,7 @@ use rand::{SeedableRng, rngs::StdRng}; /// /// See: https://github.com/coinbase/kryptology/blob/1dcc062313d99f2e56ce6abc2003ef63c52dd4a5/test/frost_dkg/bls/main.go#L23 #[test] -fn check_kryptology_bls_round_trip_2_of_4_ctx_0() { +fn kryptology_bls_round_trip_2_of_4_ctx_0() { let mut rng = StdRng::seed_from_u64(20260410); let threshold = 2u16; let max_signers = 4u16; From eb7e70e437194d7e7ed4aac6feb00e9ee070b088 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 18:27:16 -0300 Subject: [PATCH 15/23] Enable lints --- crates/frost/Cargo.toml | 16 ++++++++++++++++ crates/frost/tests/kryptology_interop.rs | 2 ++ crates/frost/tests/kryptology_round_trip.rs | 2 ++ 3 files changed, 20 insertions(+) diff --git a/crates/frost/Cargo.toml b/crates/frost/Cargo.toml index 5fffcc41..f2b05c41 100644 --- a/crates/frost/Cargo.toml +++ b/crates/frost/Cargo.toml @@ -15,3 +15,19 @@ sha2.workspace = true hex.workspace = true rand.workspace = true serde_json.workspace = true + +[lints.rust] +missing_docs = "deny" +# Allow unsafe code for blst C bindings (overrides workspace forbid) +unsafe_code = "allow" + +[lints.clippy] +arithmetic_side_effects = "deny" +cast_lossless = "deny" +cast_possible_truncation = "deny" +cast_possible_wrap = "deny" +cast_precision_loss = "deny" +cast_sign_loss = "deny" +needless_return = "deny" +panicking_overflow_checks = "deny" +unwrap_used = "deny" diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs index 45b4b72e..53357182 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/tests/kryptology_interop.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::collections::BTreeMap; use pluto_frost::kryptology; diff --git a/crates/frost/tests/kryptology_round_trip.rs b/crates/frost/tests/kryptology_round_trip.rs index e4752aeb..6a473b9d 100644 --- a/crates/frost/tests/kryptology_round_trip.rs +++ b/crates/frost/tests/kryptology_round_trip.rs @@ -1,3 +1,5 @@ +#![allow(missing_docs)] + use std::collections::BTreeMap; use pluto_frost::kryptology; From 1832c1cc3a4b281829d79e2e53fcaf74b0d2fe9a Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 18:52:01 -0300 Subject: [PATCH 16/23] Use derive macros when possible - Replaces custom deserialization code --- Cargo.lock | 1 + crates/frost/Cargo.toml | 1 + crates/frost/tests/kryptology_interop.rs | 219 +++++++++++------------ 3 files changed, 105 insertions(+), 116 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6a113a24..0d3aaaae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5677,6 +5677,7 @@ dependencies = [ "hex", "rand 0.8.5", "rand_core 0.6.4", + "serde", "serde_json", "sha2", ] diff --git a/crates/frost/Cargo.toml b/crates/frost/Cargo.toml index f2b05c41..9a5192aa 100644 --- a/crates/frost/Cargo.toml +++ b/crates/frost/Cargo.toml @@ -14,6 +14,7 @@ sha2.workspace = true [dev-dependencies] hex.workspace = true rand.workspace = true +serde.workspace = true serde_json.workspace = true [lints.rust] diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs index 53357182..4d87a928 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/tests/kryptology_interop.rs @@ -3,7 +3,7 @@ use std::collections::BTreeMap; use pluto_frost::kryptology; -use serde_json::Value; +use serde::Deserialize; const FIXTURE_2_OF_3_CTX_0: &str = include_str!("./kryptology_fixtures/2-of-3-ctx-0.json"); const FIXTURE_3_OF_3_CTX_0: &str = include_str!("./kryptology_fixtures/3-of-3-ctx-0.json"); @@ -11,24 +11,49 @@ const FIXTURE_MALFORMED_SHARE_ID: &str = include_str!("./kryptology_fixtures/malformed-share-id.json"); const FIXTURE_INVALID_PROOF: &str = include_str!("./kryptology_fixtures/invalid-proof.json"); -#[derive(Clone)] +#[derive(Clone, Deserialize)] struct FixtureParticipant { id: u32, + #[serde(deserialize_with = "hex_serde::hex_32")] own_share: [u8; 32], - round1_bcast: kryptology::Round1Bcast, - shares_sent: BTreeMap, + round1_bcast: FixtureRound1Bcast, + shares_sent: Vec, expected_round2: ExpectedRound2, } -#[derive(Clone)] +#[derive(Clone, Deserialize)] +struct FixtureRound1Bcast { + #[serde(deserialize_with = "hex_serde::hex_48_vec")] + commitments: Vec<[u8; 48]>, + #[serde(deserialize_with = "hex_serde::hex_32")] + wi: [u8; 32], + #[serde(deserialize_with = "hex_serde::hex_32")] + ci: [u8; 32], +} + +#[derive(Clone, Deserialize)] +struct FixtureShamirShare { + to: u32, + id: u32, + #[serde(deserialize_with = "hex_serde::hex_32")] + value: [u8; 32], +} + +#[derive(Clone, Deserialize)] +#[serde(tag = "kind", rename_all = "snake_case")] enum ExpectedRound2 { Success { + #[serde(deserialize_with = "hex_serde::hex_48")] verification_key: [u8; 48], + #[serde(deserialize_with = "hex_serde::hex_48")] vk_share: [u8; 48], + #[serde(deserialize_with = "hex_serde::hex_32")] signing_share: [u8; 32], }, - Error { - kind: String, + InvalidShare { + culprit: u32, + }, + InvalidProof { culprit: u32, }, } @@ -40,6 +65,16 @@ struct FixtureScenario { participants: BTreeMap, } +impl From<&FixtureRound1Bcast> for kryptology::Round1Bcast { + fn from(f: &FixtureRound1Bcast) -> Self { + Self { + commitments: f.commitments.clone(), + wi: f.wi, + ci: f.ci, + } + } +} + #[test] fn kryptology_fixture_round2_interop_2_of_3_ctx_0() { replay_fixture(FIXTURE_2_OF_3_CTX_0, true); @@ -71,14 +106,32 @@ fn replay_fixture(json: &str, require_group_signature: bool) { .participants .iter() .filter(|&(&sender_id, _)| sender_id != id) - .map(|(&sender_id, sender)| (sender_id, sender.round1_bcast.clone())) + .map(|(&sender_id, sender)| { + ( + sender_id, + kryptology::Round1Bcast::from(&sender.round1_bcast), + ) + }) .collect(); let received_shares = scenario .participants .iter() .filter(|&(&sender_id, _)| sender_id != id) - .map(|(&sender_id, sender)| (sender_id, sender.shares_sent[&id].clone())) + .map(|(&sender_id, sender)| { + let s = sender + .shares_sent + .iter() + .find(|s| s.to == id) + .expect("share for recipient"); + ( + sender_id, + kryptology::ShamirShare { + id: s.id, + value: s.value, + }, + ) + }) .collect(); let secret = kryptology::Round1Secret::from_raw( @@ -110,17 +163,19 @@ fn replay_fixture(json: &str, require_group_signature: bool) { key_packages.insert(id, key_package); public_key_packages.push(public_key_package); } - ExpectedRound2::Error { kind, culprit } => { + ExpectedRound2::InvalidShare { culprit } => { let err = result.expect_err("round2 should fail"); - match (kind.as_str(), err) { - ("invalid_share", kryptology::DkgError::InvalidShare { culprit: got }) => { - assert_eq!(got, *culprit); - } - ("invalid_proof", kryptology::DkgError::InvalidProof { culprit: got }) => { - assert_eq!(got, *culprit); - } - (expected, other) => panic!("expected {expected}, got {other:?}"), - } + assert!( + matches!(err, kryptology::DkgError::InvalidShare { culprit: c } if c == *culprit), + "expected InvalidShare(culprit={culprit}), got {err:?}" + ); + } + ExpectedRound2::InvalidProof { culprit } => { + let err = result.expect_err("round2 should fail"); + assert!( + matches!(err, kryptology::DkgError::InvalidProof { culprit: c } if c == *culprit), + "expected InvalidProof(culprit={culprit}), got {err:?}" + ); } } } @@ -152,113 +207,45 @@ fn replay_fixture(json: &str, require_group_signature: bool) { } fn parse_fixture(json: &str) -> FixtureScenario { - let root: Value = serde_json::from_str(json).expect("fixture JSON should parse"); - let threshold = get_u64(&root, "threshold") as u16; - let max_signers = get_u64(&root, "max_signers") as u16; - let ctx = get_u64(&root, "ctx") as u8; - - let participants = get_array(&root, "participants") - .iter() - .map(parse_participant) - .map(|participant| (participant.id, participant)) - .collect(); + #[derive(Deserialize)] + struct RawScenario { + threshold: u16, + max_signers: u16, + ctx: u8, + participants: Vec, + } + let raw: RawScenario = serde_json::from_str(json).expect("fixture JSON should parse"); FixtureScenario { - threshold, - max_signers, - ctx, - participants, + threshold: raw.threshold, + max_signers: raw.max_signers, + ctx: raw.ctx, + participants: raw.participants.into_iter().map(|p| (p.id, p)).collect(), } } -fn parse_participant(value: &Value) -> FixtureParticipant { - let id = get_u64(value, "id") as u32; - let own_share = decode_hex_32(get_str(value, "own_share")); - let round1_bcast = parse_round1_bcast(get_value(value, "round1_bcast")); +mod hex_serde { + use serde::Deserialize; - let shares_sent = get_array(value, "shares_sent") - .iter() - .map(|share| { - let recipient = get_u64(share, "to") as u32; - let wire = kryptology::ShamirShare { - id: get_u64(share, "id") as u32, - value: decode_hex_32(get_str(share, "value")), - }; - (recipient, wire) - }) - .collect(); - - let expected_round2 = parse_expected_round2(get_value(value, "expected_round2")); - - FixtureParticipant { - id, - own_share, - round1_bcast, - shares_sent, - expected_round2, + fn decode_hex(s: &str) -> Result<[u8; N], String> { + hex::decode(s) + .map_err(|e| e.to_string())? + .try_into() + .map_err(|_| format!("expected {N} bytes")) } -} - -fn parse_round1_bcast(value: &Value) -> kryptology::Round1Bcast { - let commitments = get_array(value, "commitments") - .iter() - .map(|commitment| decode_hex_48(commitment.as_str().expect("hex string"))) - .collect(); - kryptology::Round1Bcast { - commitments, - wi: decode_hex_32(get_str(value, "wi")), - ci: decode_hex_32(get_str(value, "ci")), + pub fn hex_32<'de, D: serde::Deserializer<'de>>(d: D) -> Result<[u8; 32], D::Error> { + decode_hex(<&str>::deserialize(d)?).map_err(serde::de::Error::custom) } -} -fn parse_expected_round2(value: &Value) -> ExpectedRound2 { - let kind = get_str(value, "kind"); - match kind { - "success" => ExpectedRound2::Success { - verification_key: decode_hex_48(get_str(value, "verification_key")), - vk_share: decode_hex_48(get_str(value, "vk_share")), - signing_share: decode_hex_32(get_str(value, "signing_share")), - }, - "invalid_share" | "invalid_proof" => ExpectedRound2::Error { - kind: String::from(kind), - culprit: get_u64(value, "culprit") as u32, - }, - other => panic!("unsupported expected_round2 kind: {other}"), + pub fn hex_48<'de, D: serde::Deserializer<'de>>(d: D) -> Result<[u8; 48], D::Error> { + decode_hex(<&str>::deserialize(d)?).map_err(serde::de::Error::custom) } -} - -fn get_value<'a>(value: &'a Value, key: &str) -> &'a Value { - value - .get(key) - .unwrap_or_else(|| panic!("missing key: {key}")) -} -fn get_array<'a>(value: &'a Value, key: &str) -> &'a [Value] { - get_value(value, key) - .as_array() - .map(Vec::as_slice) - .unwrap_or_else(|| panic!("{key} should be an array")) -} - -fn get_str<'a>(value: &'a Value, key: &str) -> &'a str { - get_value(value, key) - .as_str() - .unwrap_or_else(|| panic!("{key} should be a string")) -} - -fn get_u64(value: &Value, key: &str) -> u64 { - get_value(value, key) - .as_u64() - .unwrap_or_else(|| panic!("{key} should be a u64")) -} - -fn decode_hex_32(hex_value: &str) -> [u8; 32] { - let bytes = hex::decode(hex_value).expect("hex should decode"); - bytes.try_into().expect("expected 32-byte value") -} - -fn decode_hex_48(hex_value: &str) -> [u8; 48] { - let bytes = hex::decode(hex_value).expect("hex should decode"); - bytes.try_into().expect("expected 48-byte value") + pub fn hex_48_vec<'de, D: serde::Deserializer<'de>>(d: D) -> Result, D::Error> { + Vec::::deserialize(d)? + .iter() + .map(|s| decode_hex(s).map_err(serde::de::Error::custom)) + .collect() + } } From 24fb183965b838840822497e2c02f59cf0c1b1f9 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 18:53:03 -0300 Subject: [PATCH 17/23] Inline const --- crates/frost/tests/kryptology_interop.rs | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs index 4d87a928..8bd5e9f0 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/tests/kryptology_interop.rs @@ -5,12 +5,6 @@ use std::collections::BTreeMap; use pluto_frost::kryptology; use serde::Deserialize; -const FIXTURE_2_OF_3_CTX_0: &str = include_str!("./kryptology_fixtures/2-of-3-ctx-0.json"); -const FIXTURE_3_OF_3_CTX_0: &str = include_str!("./kryptology_fixtures/3-of-3-ctx-0.json"); -const FIXTURE_MALFORMED_SHARE_ID: &str = - include_str!("./kryptology_fixtures/malformed-share-id.json"); -const FIXTURE_INVALID_PROOF: &str = include_str!("./kryptology_fixtures/invalid-proof.json"); - #[derive(Clone, Deserialize)] struct FixtureParticipant { id: u32, @@ -77,22 +71,34 @@ impl From<&FixtureRound1Bcast> for kryptology::Round1Bcast { #[test] fn kryptology_fixture_round2_interop_2_of_3_ctx_0() { - replay_fixture(FIXTURE_2_OF_3_CTX_0, true); + replay_fixture( + include_str!("./kryptology_fixtures/2-of-3-ctx-0.json"), + true, + ); } #[test] fn kryptology_fixture_round2_interop_3_of_3_ctx_0() { - replay_fixture(FIXTURE_3_OF_3_CTX_0, true); + replay_fixture( + include_str!("./kryptology_fixtures/3-of-3-ctx-0.json"), + true, + ); } #[test] fn kryptology_fixture_round2_interop_malformed_share_id() { - replay_fixture(FIXTURE_MALFORMED_SHARE_ID, false); + replay_fixture( + include_str!("./kryptology_fixtures/malformed-share-id.json"), + false, + ); } #[test] fn kryptology_fixture_round2_interop_invalid_proof() { - replay_fixture(FIXTURE_INVALID_PROOF, false); + replay_fixture( + include_str!("./kryptology_fixtures/invalid-proof.json"), + false, + ); } fn replay_fixture(json: &str, require_group_signature: bool) { From e83c631a6667446253db57c59c76e6df418bcd54 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 19:02:42 -0300 Subject: [PATCH 18/23] Simplify fixture parsing - Use vec directly --- crates/frost/tests/kryptology_interop.rs | 38 +++++++----------------- 1 file changed, 11 insertions(+), 27 deletions(-) diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/tests/kryptology_interop.rs index 8bd5e9f0..432e8ebd 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/tests/kryptology_interop.rs @@ -52,11 +52,12 @@ enum ExpectedRound2 { }, } +#[derive(Deserialize)] struct FixtureScenario { threshold: u16, max_signers: u16, ctx: u8, - participants: BTreeMap, + participants: Vec, } impl From<&FixtureRound1Bcast> for kryptology::Round1Bcast { @@ -102,19 +103,20 @@ fn kryptology_fixture_round2_interop_invalid_proof() { } fn replay_fixture(json: &str, require_group_signature: bool) { - let scenario = parse_fixture(json); + let scenario: FixtureScenario = serde_json::from_str(json).expect("invalid fixture JSON"); let mut key_packages = BTreeMap::new(); let mut public_key_packages = Vec::new(); - for (&id, participant) in &scenario.participants { + for participant in &scenario.participants { + let id = participant.id; let received_bcasts = scenario .participants .iter() - .filter(|&(&sender_id, _)| sender_id != id) - .map(|(&sender_id, sender)| { + .filter(|&sender| sender.id != id) + .map(|sender| { ( - sender_id, + sender.id, kryptology::Round1Bcast::from(&sender.round1_bcast), ) }) @@ -123,15 +125,15 @@ fn replay_fixture(json: &str, require_group_signature: bool) { let received_shares = scenario .participants .iter() - .filter(|&(&sender_id, _)| sender_id != id) - .map(|(&sender_id, sender)| { + .filter(|&sender| sender.id != id) + .map(|sender| { let s = sender .shares_sent .iter() .find(|s| s.to == id) .expect("share for recipient"); ( - sender_id, + sender.id, kryptology::ShamirShare { id: s.id, value: s.value, @@ -212,24 +214,6 @@ fn replay_fixture(json: &str, require_group_signature: bool) { ); } -fn parse_fixture(json: &str) -> FixtureScenario { - #[derive(Deserialize)] - struct RawScenario { - threshold: u16, - max_signers: u16, - ctx: u8, - participants: Vec, - } - - let raw: RawScenario = serde_json::from_str(json).expect("fixture JSON should parse"); - FixtureScenario { - threshold: raw.threshold, - max_signers: raw.max_signers, - ctx: raw.ctx, - participants: raw.participants.into_iter().map(|p| (p.id, p)).collect(), - } -} - mod hex_serde { use serde::Deserialize; From 44c11a40500f119233cdee682266b864da1de2c8 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 19:14:16 -0300 Subject: [PATCH 19/23] Fix clippy lints - Infallible conversions - Disable arithmetic checks --- crates/frost/src/frost_core.rs | 4 +++- crates/frost/src/kryptology.rs | 10 ++++++---- crates/frost/src/tests.rs | 8 ++++---- crates/frost/tests/kryptology_round_trip.rs | 4 ++-- 4 files changed, 15 insertions(+), 11 deletions(-) diff --git a/crates/frost/src/frost_core.rs b/crates/frost/src/frost_core.rs index 2354ffda..b01f230a 100644 --- a/crates/frost/src/frost_core.rs +++ b/crates/frost/src/frost_core.rs @@ -4,6 +4,8 @@ //! Contains the key material types (identifiers, shares, packages) and the //! polynomial evaluation functions needed by the kryptology-compatible DKG. +#![allow(clippy::arithmetic_side_effects)] + use std::{ cmp::Ordering, collections::{BTreeMap, BTreeSet}, @@ -37,7 +39,7 @@ pub struct Identifier(Scalar); impl Identifier { /// Create a new identifier from a non-zero u32. pub fn from_u32(id: u32) -> Result { - let scalar = Scalar::from(id as u64); + let scalar = Scalar::from(u64::from(id)); if scalar == Scalar::ZERO { Err(FrostCoreError::InvalidZeroScalar) } else { diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index 26454ba6..87225b88 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -9,6 +9,8 @@ //! The output types ([`KeyPackage`], [`PublicKeyPackage`]) are standard //! frost-core types usable with frost-core's signing protocol. +#![allow(clippy::arithmetic_side_effects)] + use std::collections::BTreeMap; use blst::*; @@ -270,13 +272,13 @@ pub fn round1( rng: &mut R, ) -> Result<(Round1Bcast, BTreeMap, Round1Secret), DkgError> { // Kryptology encodes participant identifiers into a single byte. - if max_signers > u8::MAX as u16 { + if max_signers > u16::from(u8::MAX) { return Err(DkgError::InvalidSignerCount); } validate_num_of_signers(threshold, max_signers)?; - if id == 0 || id > max_signers as u32 { + if id == 0 || id > u32::from(max_signers) { return Err(DkgError::InvalidParticipantId(id)); } @@ -310,7 +312,7 @@ pub fn round1( // Pre-compute Shamir shares for every other participant let mut shares = BTreeMap::new(); - for j in 1..=max_signers as u32 { + for j in 1..=u32::from(max_signers) { if j == id { continue; } @@ -524,7 +526,7 @@ impl BlsSignature { let x_vals: Vec = partial_sigs .iter() - .map(|ps| Scalar::from(ps.identifier as u64)) + .map(|ps| Scalar::from(u64::from(ps.identifier))) .collect(); let mut combined = blst_p2::default(); diff --git a/crates/frost/src/tests.rs b/crates/frost/src/tests.rs index 64f913df..8e8013a2 100644 --- a/crates/frost/src/tests.rs +++ b/crates/frost/src/tests.rs @@ -88,7 +88,7 @@ fn kryptology_bls_round_trip_3_of_3() { let mut all_shares: BTreeMap> = BTreeMap::new(); let mut secrets: BTreeMap = BTreeMap::new(); - for id in 1..=max_signers as u32 { + for id in 1..=u32::from(max_signers) { let (bcast, shares, secret) = kryptology::round1(id, threshold, max_signers, ctx, &mut rng) .expect("round1 should succeed"); bcasts.insert(id, bcast); @@ -107,7 +107,7 @@ fn kryptology_bls_round_trip_3_of_3() { let mut public_key_packages = Vec::new(); let mut round2_bcasts = BTreeMap::new(); - for id in 1..=max_signers as u32 { + for id in 1..=u32::from(max_signers) { // Collect broadcasts from everyone except ourselves let received_bcasts: BTreeMap = bcasts .iter() @@ -178,7 +178,7 @@ fn kryptology_bls_round_trip_2_of_3() { let mut all_shares: BTreeMap> = BTreeMap::new(); let mut secrets: BTreeMap = BTreeMap::new(); - for id in 1..=max_signers as u32 { + for id in 1..=u32::from(max_signers) { let (bcast, shares, secret) = kryptology::round1(id, threshold, max_signers, ctx, &mut rng).unwrap(); bcasts.insert(id, bcast); @@ -195,7 +195,7 @@ fn kryptology_bls_round_trip_2_of_3() { let mut key_packages = BTreeMap::new(); let mut public_key_packages = Vec::new(); - for id in 1..=max_signers as u32 { + for id in 1..=u32::from(max_signers) { let received_bcasts: BTreeMap<_, _> = bcasts .iter() .filter(|(k, _)| **k != id) diff --git a/crates/frost/tests/kryptology_round_trip.rs b/crates/frost/tests/kryptology_round_trip.rs index 6a473b9d..e0335f46 100644 --- a/crates/frost/tests/kryptology_round_trip.rs +++ b/crates/frost/tests/kryptology_round_trip.rs @@ -21,7 +21,7 @@ fn kryptology_bls_round_trip_2_of_4_ctx_0() { let mut round1_shares: BTreeMap> = BTreeMap::new(); let mut round1_secrets = BTreeMap::new(); - for id in 1..=max_signers as u32 { + for id in 1..=u32::from(max_signers) { let (bcast, shares, secret) = kryptology::round1(id, threshold, max_signers, ctx, &mut rng) .expect("round1 should succeed for each participant"); @@ -48,7 +48,7 @@ fn kryptology_bls_round_trip_2_of_4_ctx_0() { let mut key_packages = BTreeMap::new(); let mut public_key_packages = BTreeMap::new(); - for id in 1..=max_signers as u32 { + for id in 1..=u32::from(max_signers) { let received_bcasts: BTreeMap = round1_bcasts .iter() .filter(|&(sender_id, _)| *sender_id != id) From 3952fb1f540d8a44141f0aa44e57e00fa32d9699 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Tue, 14 Apr 2026 19:24:09 -0300 Subject: [PATCH 20/23] Assert conversions - Use `assert!` over `debug_assert!` - Reference RFC when required --- crates/frost/src/kryptology.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index 87225b88..865a2414 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -162,10 +162,17 @@ pub fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { const S_IN_BYTES: usize = 64; // SHA-256 block size let ell = len_in_bytes.div_ceil(B_IN_BYTES); - debug_assert!(ell <= 255 && len_in_bytes <= 65535 && dst.len() <= 255); + assert!(ell <= 255, "RFC 9380: ell must be at most 255"); + assert!( + len_in_bytes <= 65535, + "RFC 9380: len_in_bytes must fit in 2 bytes" + ); + assert!(dst.len() <= 255, "RFC 9380: DST must be at most 255 bytes"); - let dst_prime_suffix = [dst.len() as u8]; - let l_i_b_str = [(len_in_bytes >> 8) as u8, (len_in_bytes & 0xff) as u8]; + let dst_prime_suffix = [u8::try_from(dst.len()).expect("asserted above")]; + let l_i_b_str = u16::try_from(len_in_bytes) + .expect("asserted above") + .to_be_bytes(); // b_0 = H(Z_pad || msg || l_i_b_str || I2OSP(0,1) || DST_prime) let mut h0 = Sha256::new(); @@ -196,7 +203,7 @@ pub fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { } let mut hi = Sha256::new(); hi.update(xored); - hi.update([i as u8]); + hi.update([u8::try_from(i).expect("ell <= 255 asserted above")]); hi.update(dst); hi.update(dst_prime_suffix); let b_i: [u8; 32] = hi.finalize().into(); @@ -307,7 +314,8 @@ pub fn round1( } }; let r_point = G1Projective::generator() * k; - let ci = kryptology_challenge(id as u8, ctx, &commitment_points[0], &r_point); + let id_u8 = u8::try_from(id).expect("id <= max_signers <= u8::MAX validated above"); + let ci = kryptology_challenge(id_u8, ctx, &commitment_points[0], &r_point); let wi = k + coefficients[0] * ci; // Pre-compute Shamir shares for every other participant @@ -389,7 +397,9 @@ pub fn round2( // Reconstruct R' = Wi*G - Ci*A_{j,0} let r_reconstructed = G1Projective::generator() * wi - a0 * ci; - let ci_check = kryptology_challenge(sender_id as u8, secret.ctx, &a0, &r_reconstructed); + let sender_id_u8 = + u8::try_from(sender_id).map_err(|_| DkgError::InvalidParticipantId(sender_id))?; + let ci_check = kryptology_challenge(sender_id_u8, secret.ctx, &a0, &r_reconstructed); if ci_check != ci { return Err(DkgError::InvalidProof { culprit: sender_id }); } From 9830515bfd7b2bece388db1f14986aba65dd8a22 Mon Sep 17 00:00:00 2001 From: Lautaro Emanuel Date: Wed, 15 Apr 2026 11:01:20 -0300 Subject: [PATCH 21/23] Update lockfile --- Cargo.lock | 335 ++++++++++++++++++++++++++++------------------------- 1 file changed, 175 insertions(+), 160 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0d3aaaae..a3bf51eb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -104,9 +104,9 @@ dependencies = [ [[package]] name = "alloy-chains" -version = "0.2.33" +version = "0.2.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4e9e31d834fe25fe991b8884e4b9f0e59db4a97d86e05d1464d6899c013cd62" +checksum = "84e0378e959aa6a885897522080a990e80eb317f1e9a222a604492ea50e13096" dependencies = [ "alloy-primitives", "num_enum", @@ -373,13 +373,13 @@ dependencies = [ "derive_more", "foldhash 0.2.0", "hashbrown 0.16.1", - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "k256", "keccak-asm", "paste", "proptest", - "rand 0.9.3", + "rand 0.9.4", "rapidhash", "ruint", "rustc-hash", @@ -428,9 +428,9 @@ dependencies = [ [[package]] name = "alloy-rlp" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e93e50f64a77ad9c5470bf2ad0ca02f228da70c792a8f06634801e202579f35e" +checksum = "dc90b1e703d3c03f4ff7f48e82dd0bc1c8211ab7d079cd836a06fcfeb06651cb" dependencies = [ "alloy-rlp-derive", "arrayvec", @@ -439,9 +439,9 @@ dependencies = [ [[package]] name = "alloy-rlp-derive" -version = "0.3.13" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce8849c74c9ca0f5a03da1c865e3eb6f768df816e67dd3721a398a8a7e398011" +checksum = "f36834a5c0a2fa56e171bf256c34d70fca07d0c0031583edea1c4946b7889c9e" dependencies = [ "proc-macro2", "quote", @@ -581,7 +581,7 @@ dependencies = [ "alloy-sol-macro-input", "const-hex", "heck", - "indexmap 2.13.0", + "indexmap 2.14.0", "proc-macro-error2", "proc-macro2", "quote", @@ -1175,9 +1175,9 @@ dependencies = [ [[package]] name = "axum" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90" dependencies = [ "axum-core", "bytes", @@ -1326,9 +1326,9 @@ dependencies = [ [[package]] name = "bitflags" -version = "2.11.0" +version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3" [[package]] name = "bitvec" @@ -1398,7 +1398,7 @@ dependencies = [ "log", "num", "pin-project-lite", - "rand 0.9.3", + "rand 0.9.4", "rustls", "rustls-native-certs", "rustls-pki-types", @@ -1569,7 +1569,7 @@ version = "10.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06acb4f71407ba205a07cb453211e0e6a67b21904e47f6ba1f9589e38f2e454" dependencies = [ - "semver 1.0.27", + "semver 1.0.28", "serde", "toml", "url", @@ -1592,9 +1592,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.58" +version = "1.2.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" +checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" dependencies = [ "find-msvc-tools", "jobserver", @@ -1855,15 +1855,6 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - [[package]] name = "cpufeatures" version = "0.2.17" @@ -2187,7 +2178,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -2527,9 +2518,9 @@ dependencies = [ [[package]] name = "ethereum_ssz" -version = "0.10.1" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2128a84f7a3850d54ee343334e3392cca61f9f6aa9441eec481b9394b43c238b" +checksum = "368a4a4e4273b0135111fe9464e35465067766a8f664615b5a86338b73864407" dependencies = [ "alloy-primitives", "ethereum_serde_utils", @@ -2574,9 +2565,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.3.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" [[package]] name = "fastrlp" @@ -2607,7 +2598,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb330bbd4cb7a5b9f559427f06f98a4f853a137c8298f3bd3f8ca57663e21986" dependencies = [ "portable-atomic", - "rand 0.9.3", + "rand 0.9.4", "web-time", ] @@ -2999,7 +2990,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "slab", "tokio", "tokio-util", @@ -3054,6 +3045,12 @@ dependencies = [ "serde_core", ] +[[package]] +name = "hashbrown" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51" + [[package]] name = "hashlink" version = "0.9.1" @@ -3072,6 +3069,15 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "hashlink" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea0b22561a9c04a7cb1a302c013e0259cd3b4bb619f145b32f72b8b4bcbed230" +dependencies = [ + "hashbrown 0.16.1", +] + [[package]] name = "heck" version = "0.5.0" @@ -3127,7 +3133,7 @@ dependencies = [ "idna", "ipnet", "once_cell", - "rand 0.9.3", + "rand 0.9.4", "ring", "socket2 0.5.10", "thiserror 2.0.18", @@ -3150,7 +3156,7 @@ dependencies = [ "moka", "once_cell", "parking_lot", - "rand 0.9.3", + "rand 0.9.4", "resolv-conf", "smallvec", "thiserror 2.0.18", @@ -3238,9 +3244,9 @@ checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424" [[package]] name = "hyper" -version = "1.8.1" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca" dependencies = [ "atomic-waker", "bytes", @@ -3253,7 +3259,6 @@ dependencies = [ "httpdate", "itoa", "pin-project-lite", - "pin-utils", "smallvec", "tokio", "want", @@ -3276,15 +3281,14 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.27.7" +version = "0.27.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f" dependencies = [ "http", "hyper", "hyper-util", "rustls", - "rustls-pki-types", "tokio", "tokio-rustls", "tower-service", @@ -3385,12 +3389,13 @@ dependencies = [ [[package]] name = "icu_collections" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43" +checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c" dependencies = [ "displaydoc", "potential_utf", + "utf8_iter", "yoke", "zerofrom", "zerovec", @@ -3398,9 +3403,9 @@ dependencies = [ [[package]] name = "icu_locale_core" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6" +checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29" dependencies = [ "displaydoc", "litemap", @@ -3411,9 +3416,9 @@ dependencies = [ [[package]] name = "icu_normalizer" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599" +checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4" dependencies = [ "icu_collections", "icu_normalizer_data", @@ -3425,15 +3430,15 @@ dependencies = [ [[package]] name = "icu_normalizer_data" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a" +checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38" [[package]] name = "icu_properties" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "020bfc02fe870ec3a66d93e677ccca0562506e5872c650f893269e08615d74ec" +checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de" dependencies = [ "icu_collections", "icu_locale_core", @@ -3445,15 +3450,15 @@ dependencies = [ [[package]] name = "icu_properties_data" -version = "2.1.2" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616c294cf8d725c6afcd8f55abc17c56464ef6211f9ed59cccffe534129c77af" +checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14" [[package]] name = "icu_provider" -version = "2.1.1" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614" +checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421" dependencies = [ "displaydoc", "icu_locale_core", @@ -3545,7 +3550,7 @@ dependencies = [ "hyper", "hyper-util", "log", - "rand 0.9.3", + "rand 0.9.4", "tokio", "url", "xmltree", @@ -3584,12 +3589,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.13.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown 0.16.1", + "hashbrown 0.17.0", "serde", "serde_core", ] @@ -3727,9 +3732,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.93" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797146bb2677299a1eb6b7b50a890f4c361b29ef967addf5b2fa45dae1bb6d7d" +checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" dependencies = [ "cfg-if", "futures-util", @@ -3785,9 +3790,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.183" +version = "0.2.185" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" +checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" [[package]] name = "libgit2-sys" @@ -4263,15 +4268,16 @@ dependencies = [ [[package]] name = "libp2p-rendezvous" -version = "0.17.0" +version = "0.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "15285d828c2b4a34cb660c2e74cd6938116daceab1f4357bae933d5b08cca933" +checksum = "31114bab295403e9934ae2e4415c45d681353829ea218390eed8f5bcc82dd1fb" dependencies = [ "async-trait", "asynchronous-codec", "bimap", "futures", "futures-timer", + "hashlink 0.11.0", "libp2p-core", "libp2p-identity", "libp2p-request-response", @@ -4520,21 +4526,21 @@ dependencies = [ [[package]] name = "libredox" -version = "0.1.15" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ddbf48fd451246b1f8c2610bd3b4ac0cc6e149d89832867093ab69a17194f08" +checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c" dependencies = [ "bitflags", "libc", "plain", - "redox_syscall 0.7.3", + "redox_syscall 0.7.4", ] [[package]] name = "libz-sys" -version = "1.1.25" +version = "1.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52f4c29e2a68ac30c9087e1b772dc9f44a2b66ed44edf2266cf2be9b03dafc1" +checksum = "fc3a226e576f50782b3305c5ccf458698f92798987f551c6a02efe8276721e22" dependencies = [ "cc", "libc", @@ -4550,9 +4556,9 @@ checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77" +checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "lock_api" @@ -4581,9 +4587,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.16.3" +version = "0.16.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" +checksum = "7f66e8d5d03f609abc3a39e6f08e4164ebf1447a732906d39eb9b99b7919ef39" dependencies = [ "hashbrown 0.16.1", ] @@ -4740,11 +4746,11 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.3" +version = "0.19.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" +checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c" dependencies = [ - "core2", + "no_std_io2", "serde", "unsigned-varint 0.8.0", ] @@ -4846,6 +4852,15 @@ dependencies = [ "libc", ] +[[package]] +name = "no_std_io2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6" +dependencies = [ + "memchr", +] + [[package]] name = "nohash-hasher" version = "0.2.0" @@ -5016,7 +5031,7 @@ dependencies = [ "log", "once_cell", "regex", - "semver 1.0.27", + "semver 1.0.28", "serde", "serde_json", "serde_yaml", @@ -5036,7 +5051,7 @@ dependencies = [ "eventsource-stream", "futures-core", "http", - "indexmap 2.13.0", + "indexmap 2.14.0", "oas3", "prettyplease", "proc-macro2", @@ -5093,9 +5108,9 @@ checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" [[package]] name = "openssl" -version = "0.10.76" +version = "0.10.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "951c002c75e16ea2c65b8c7e4d3d51d5530d8dfa7d060b4776828c88cfb18ecf" +checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" dependencies = [ "bitflags", "cfg-if", @@ -5125,9 +5140,9 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "openssl-sys" -version = "0.9.112" +version = "0.9.113" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57d55af3b3e226502be1526dfdba67ab0e9c96fc293004e79576b2b9edb0dbdb" +checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" dependencies = [ "cc", "libc", @@ -5309,7 +5324,7 @@ checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" dependencies = [ "fixedbitset", "hashbrown 0.15.5", - "indexmap 2.13.0", + "indexmap 2.14.0", ] [[package]] @@ -5356,9 +5371,9 @@ dependencies = [ [[package]] name = "pkg-config" -version = "0.3.32" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] name = "plain" @@ -5868,9 +5883,9 @@ checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "potential_utf" -version = "0.1.4" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77" +checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564" dependencies = [ "zerovec", ] @@ -5926,7 +5941,7 @@ version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f" dependencies = [ - "toml_edit 0.25.8+spec-1.1.0", + "toml_edit 0.25.11+spec-1.1.0", ] [[package]] @@ -5993,7 +6008,7 @@ dependencies = [ "bit-vec", "bitflags", "num-traits", - "rand 0.9.3", + "rand 0.9.4", "rand_chacha 0.9.0", "rand_xorshift", "regex-syntax", @@ -6154,7 +6169,7 @@ dependencies = [ "bytes", "getrandom 0.3.4", "lru-slab", - "rand 0.9.3", + "rand 0.9.4", "ring", "rustc-hash", "rustls", @@ -6234,9 +6249,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.9.3" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ec095654a25171c2124e9e3393a930bddbffdc939556c914957a4c3e0a87166" +checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea" dependencies = [ "rand_chacha 0.9.0", "rand_core 0.9.5", @@ -6330,9 +6345,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.11.0" +version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "368f01d005bf8fd9b1206fb6fa653e6c4a81ceb1466406b81792d87c5677a58f" +checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d" dependencies = [ "either", "rayon-core", @@ -6372,9 +6387,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce70a74e890531977d37e532c34d45e9055d2409ed08ddba14529471ed0be16" +checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a" dependencies = [ "bitflags", ] @@ -6587,7 +6602,7 @@ dependencies = [ "primitive-types", "proptest", "rand 0.8.5", - "rand 0.9.3", + "rand 0.9.4", "rlp", "ruint-macro", "serde_core", @@ -6628,7 +6643,7 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" dependencies = [ - "semver 1.0.27", + "semver 1.0.28", ] [[package]] @@ -6655,9 +6670,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.37" +version = "0.23.38" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" dependencies = [ "aws-lc-rs", "log", @@ -6720,9 +6735,9 @@ checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" [[package]] name = "rustls-webpki" -version = "0.103.10" +version = "0.103.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df33b2b81ac578cabaf06b89b0631153a3f416b0a886e8a7a1707fb51abbd1ef" +checksum = "8279bb85272c9f10811ae6a6c547ff594d6a7f3c6c6b02ee9726d1d0dcfcdd06" dependencies = [ "aws-lc-rs", "ring", @@ -6904,9 +6919,9 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.27" +version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" dependencies = [ "serde", "serde_core", @@ -6972,7 +6987,7 @@ version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "memchr", "serde", @@ -7033,7 +7048,7 @@ dependencies = [ "chrono", "hex", "indexmap 1.9.3", - "indexmap 2.13.0", + "indexmap 2.14.0", "schemars 0.9.0", "schemars 1.2.1", "serde_core", @@ -7060,7 +7075,7 @@ version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "itoa", "ryu", "serde", @@ -7591,9 +7606,9 @@ dependencies = [ [[package]] name = "tinystr" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869" +checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d" dependencies = [ "displaydoc", "zerovec", @@ -7626,9 +7641,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.50.0" +version = "1.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27ad5e34374e03cfffefc301becb44e9dc3c17584f414349ebe29ed26661822d" +checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" dependencies = [ "bytes", "libc", @@ -7643,9 +7658,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c55a2eff8b69ce66c84f85e1da1c233edc36ceb85a2058d11b0d6a3c7e7569c" +checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" dependencies = [ "proc-macro2", "quote", @@ -7720,9 +7735,9 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "1.1.0+spec-1.1.0" +version = "1.1.1+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97251a7c317e03ad83774a8752a7e81fb6067740609f75ea2b585b569a59198f" +checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7" dependencies = [ "serde_core", ] @@ -7733,7 +7748,7 @@ version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ - "indexmap 2.13.0", + "indexmap 2.14.0", "serde", "serde_spanned", "toml_datetime 0.6.11", @@ -7743,21 +7758,21 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.25.8+spec-1.1.0" +version = "0.25.11+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16bff38f1d86c47f9ff0647e6838d7bb362522bdf44006c7068c2b1e606f1f3c" +checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b" dependencies = [ - "indexmap 2.13.0", - "toml_datetime 1.1.0+spec-1.1.0", + "indexmap 2.14.0", + "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", "winnow 1.0.1", ] [[package]] name = "toml_parser" -version = "1.1.0+spec-1.1.0" +version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" +checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ "winnow 1.0.1", ] @@ -7816,7 +7831,7 @@ checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" dependencies = [ "futures-core", "futures-util", - "indexmap 2.13.0", + "indexmap 2.14.0", "pin-project-lite", "slab", "sync_wrapper", @@ -8309,9 +8324,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.116" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dc0882f7b5bb01ae8c5215a1230832694481c1a4be062fd410e12ea3da5b631" +checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" dependencies = [ "cfg-if", "once_cell", @@ -8322,9 +8337,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.66" +version = "0.4.68" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19280959e2844181895ef62f065c63e0ca07ece4771b53d89bfdb967d97cbf05" +checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8" dependencies = [ "js-sys", "wasm-bindgen", @@ -8332,9 +8347,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.116" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75973d3066e01d035dbedaad2864c398df42f8dd7b1ea057c35b8407c015b537" +checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -8342,9 +8357,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.116" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91af5e4be765819e0bcfee7322c14374dc821e35e72fa663a830bbc7dc199eac" +checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" dependencies = [ "bumpalo", "proc-macro2", @@ -8355,9 +8370,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.116" +version = "0.2.118" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9bf0406a78f02f336bf1e451799cca198e8acde4ffa278f0fb20487b150a633" +checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" dependencies = [ "unicode-ident", ] @@ -8379,7 +8394,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" dependencies = [ "anyhow", - "indexmap 2.13.0", + "indexmap 2.14.0", "wasm-encoder", "wasmparser", ] @@ -8405,8 +8420,8 @@ checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" dependencies = [ "bitflags", "hashbrown 0.15.5", - "indexmap 2.13.0", - "semver 1.0.27", + "indexmap 2.14.0", + "semver 1.0.28", ] [[package]] @@ -8425,9 +8440,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.93" +version = "0.3.95" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "749466a37ee189057f54748b200186b59a03417a117267baf3fd89cecc9fb837" +checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d" dependencies = [ "js-sys", "wasm-bindgen", @@ -8972,7 +8987,7 @@ checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" dependencies = [ "anyhow", "heck", - "indexmap 2.13.0", + "indexmap 2.14.0", "prettyplease", "syn 2.0.117", "wasm-metadata", @@ -9003,7 +9018,7 @@ checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" dependencies = [ "anyhow", "bitflags", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", "serde", "serde_derive", @@ -9022,9 +9037,9 @@ checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" dependencies = [ "anyhow", "id-arena", - "indexmap 2.13.0", + "indexmap 2.14.0", "log", - "semver 1.0.27", + "semver 1.0.28", "serde", "serde_derive", "serde_json", @@ -9034,9 +9049,9 @@ dependencies = [ [[package]] name = "writeable" -version = "0.6.2" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9" +checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4" [[package]] name = "wyz" @@ -9127,7 +9142,7 @@ dependencies = [ "nohash-hasher", "parking_lot", "pin-project", - "rand 0.9.3", + "rand 0.9.4", "static_assertions", "web-time", ] @@ -9143,9 +9158,9 @@ dependencies = [ [[package]] name = "yoke" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954" +checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca" dependencies = [ "stable_deref_trait", "yoke-derive", @@ -9154,9 +9169,9 @@ dependencies = [ [[package]] name = "yoke-derive" -version = "0.8.1" +version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" +checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e" dependencies = [ "proc-macro2", "quote", @@ -9186,18 +9201,18 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] [[package]] name = "zerofrom-derive" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1" dependencies = [ "proc-macro2", "quote", @@ -9227,9 +9242,9 @@ dependencies = [ [[package]] name = "zerotrie" -version = "0.2.3" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851" +checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf" dependencies = [ "displaydoc", "yoke", @@ -9238,9 +9253,9 @@ dependencies = [ [[package]] name = "zerovec" -version = "0.11.5" +version = "0.11.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002" +checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239" dependencies = [ "yoke", "zerofrom", @@ -9249,9 +9264,9 @@ dependencies = [ [[package]] name = "zerovec-derive" -version = "0.11.2" +version = "0.11.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" +checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555" dependencies = [ "proc-macro2", "quote", From c03936f2417ebb1a25c9bf082ba7aa725a89b2fd Mon Sep 17 00:00:00 2001 From: Quang Le Date: Sat, 2 May 2026 19:33:41 +0700 Subject: [PATCH 22/23] fix: address comments / review on frost, more tests (#364) * fix: address comments and reviews in FROST impl * fix: refine error * fix: more tests on frost core * fix: add tests for curve * fix: add and refactor tests on kryptology * fix: simplify tests * fix: address comments --- Cargo.lock | 3 + Cargo.toml | 1 + crates/frost/Cargo.toml | 3 + crates/frost/src/curve.rs | 115 +++ crates/frost/src/frost_core.rs | 114 ++- crates/frost/src/kryptology.rs | 690 +++++++++++++++--- .../kryptology_interop_tests.rs} | 21 +- .../kryptology_round_trip_tests.rs} | 7 +- crates/frost/src/lib.rs | 7 +- crates/frost/src/tests.rs | 301 -------- 10 files changed, 842 insertions(+), 420 deletions(-) rename crates/frost/{tests/kryptology_interop.rs => src/kryptology_interop_tests.rs} (88%) rename crates/frost/{tests/kryptology_round_trip.rs => src/kryptology_round_trip_tests.rs} (96%) delete mode 100644 crates/frost/src/tests.rs diff --git a/Cargo.lock b/Cargo.lock index fd33866e..5ddb53ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5700,6 +5700,9 @@ dependencies = [ "serde", "serde_json", "sha2", + "subtle", + "thiserror 2.0.18", + "zeroize", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 23d7af85..9eec693d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ cipher = "0.4.4" pbkdf2 = "0.12.2" sha2 = "0.10.9" scrypt = "0.11.0" +subtle = "2.6" unicode-normalization = "0.1.25" zeroize = "1.8.2" uuid = { version = "1.19", features = ["serde", "v4"] } diff --git a/crates/frost/Cargo.toml b/crates/frost/Cargo.toml index 9a5192aa..554c30f7 100644 --- a/crates/frost/Cargo.toml +++ b/crates/frost/Cargo.toml @@ -10,6 +10,9 @@ publish.workspace = true blst.workspace = true rand_core.workspace = true sha2.workspace = true +subtle.workspace = true +thiserror.workspace = true +zeroize = { workspace = true, features = ["derive"] } [dev-dependencies] hex.workspace = true diff --git a/crates/frost/src/curve.rs b/crates/frost/src/curve.rs index 70ebd413..2c690628 100644 --- a/crates/frost/src/curve.rs +++ b/crates/frost/src/curve.rs @@ -12,6 +12,8 @@ use std::{ use blst::*; use rand_core::{CryptoRng, RngCore}; +use subtle::ConstantTimeEq; +use zeroize::Zeroize; /// BLS12-381 scalar field element. Wrapper around `blst_fr` in Montgomery form. #[derive(Copy, Clone, Default, PartialEq, Eq)] @@ -78,6 +80,17 @@ impl Scalar { Scalar(fr) } + /// Reduce big-endian bytes modulo the scalar field order. + pub(crate) fn from_be_bytes_wide(bytes: &[u8]) -> Self { + let mut scalar = blst_scalar::default(); + let mut fr = blst_fr::default(); + unsafe { + blst_scalar_from_be_bytes(&mut scalar, bytes.as_ptr(), bytes.len()); + blst_fr_from_scalar(&mut fr, &scalar); + } + Scalar(fr) + } + /// Generate a uniformly random scalar. pub fn random(rng: &mut R) -> Self { let mut wide = [0u8; 64]; @@ -94,6 +107,17 @@ impl Scalar { unsafe { blst_fr_eucl_inverse(&mut out, &self.0) }; Some(Scalar(out)) } + + /// Compare scalar limbs without early-exit equality. + pub(crate) fn constant_time_eq(&self, other: &Self) -> bool { + self.0.l.ct_eq(&other.0.l).into() + } +} + +impl Zeroize for Scalar { + fn zeroize(&mut self) { + self.0.l.zeroize(); + } } impl From for Scalar { @@ -214,6 +238,7 @@ impl Mul for G1Projective { let mut out = blst_p1::default(); unsafe { blst_scalar_from_fr(&mut scalar, &rhs.0); + // BLS12-381 scalar field order has 255 significant bits. blst_p1_mult(&mut out, &self.0, scalar.b.as_ptr(), 255); } G1Projective(out) @@ -277,3 +302,93 @@ impl From for G1Projective { G1Projective(p) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn scalar_one_matches_blst_conversion() { + assert_eq!(Scalar::ONE, Scalar::from(1u64)); + } + + #[test] + fn scalar_round_trips_little_endian_bytes() { + let scalar = Scalar::from(42); + let bytes = scalar.to_bytes(); + + assert_eq!(Scalar::from_bytes(&bytes), Some(scalar)); + } + + #[test] + fn scalar_rejects_out_of_range_bytes() { + assert_eq!(Scalar::from_bytes(&[0xff; 32]), None); + } + + #[test] + fn scalar_from_be_bytes_wide_matches_reversed_le_wide() { + let be = [7u8; 48]; + let from_be = Scalar::from_be_bytes_wide(&be); + + let mut reversed = be; + reversed.reverse(); + let mut wide = [0u8; 64]; + wide[..48].copy_from_slice(&reversed); + + assert_eq!(from_be, Scalar::from_bytes_wide(&wide)); + } + + #[test] + fn scalar_constant_time_eq_matches_equality() { + let a = Scalar::from(42); + let b = Scalar::from(42); + let c = Scalar::from(43); + + assert!(a.constant_time_eq(&b)); + assert!(!a.constant_time_eq(&c)); + } + + #[test] + fn scalar_zeroize_clears_limbs() { + let mut scalar = Scalar::from(42); + + scalar.zeroize(); + + assert_eq!(scalar, Scalar::ZERO); + } + + #[test] + fn scalar_invert_returns_none_for_zero() { + assert_eq!(Scalar::ZERO.invert(), None); + } + + #[test] + fn scalar_invert_returns_multiplicative_inverse() { + let scalar = Scalar::from(42); + let inverse = scalar.invert().expect("non-zero scalar should invert"); + + assert_eq!(scalar * inverse, Scalar::ONE); + } + + #[test] + fn g1_projective_identity_reports_identity() { + assert!(G1Projective::identity().is_identity()); + assert!(!G1Projective::generator().is_identity()); + } + + #[test] + fn g1_projective_rejects_identity_compressed_point() { + let identity = G1Affine::from(G1Projective::identity()).to_compressed(); + + assert_eq!(G1Projective::from_compressed(&identity), None); + } + + #[test] + fn g1_affine_round_trips_generator_compressed_point() { + let generator = G1Projective::generator(); + let compressed = G1Affine::from(generator).to_compressed(); + let affine = G1Affine::from_compressed(&compressed).expect("generator should deserialize"); + + assert_eq!(G1Projective::from(affine), generator); + } +} diff --git a/crates/frost/src/frost_core.rs b/crates/frost/src/frost_core.rs index b01f230a..07585f12 100644 --- a/crates/frost/src/frost_core.rs +++ b/crates/frost/src/frost_core.rs @@ -4,29 +4,34 @@ //! Contains the key material types (identifiers, shares, packages) and the //! polynomial evaluation functions needed by the kryptology-compatible DKG. -#![allow(clippy::arithmetic_side_effects)] - use std::{ cmp::Ordering, collections::{BTreeMap, BTreeSet}, }; use super::*; +use zeroize::ZeroizeOnDrop; /// Errors from key operations. -#[derive(Debug)] +#[derive(Debug, thiserror::Error)] pub enum FrostCoreError { /// Participant ID is zero. + #[error("participant ID is zero")] InvalidZeroScalar, /// Invalid number of minimum signers (must be >= 2 and <= max_signers). + #[error("invalid minimum signer count")] InvalidMinSigners, /// Invalid number of maximum signers (must be >= 2). + #[error("invalid maximum signer count")] InvalidMaxSigners, /// The secret share verification (Feldman VSS) failed. + #[error("invalid secret share")] InvalidSecretShare, /// Commitment count mismatch during aggregation. + #[error("incorrect number of commitments")] IncorrectNumberOfCommitments, /// The commitment has no coefficients. + #[error("incorrect commitment")] IncorrectCommitment, } @@ -34,7 +39,10 @@ pub enum FrostCoreError { /// /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/identifier.rs#L14-L26 #[derive(Copy, Clone, Debug)] -pub struct Identifier(Scalar); +pub struct Identifier { + id: u32, + scalar: Scalar, +} impl Identifier { /// Create a new identifier from a non-zero u32. @@ -43,19 +51,24 @@ impl Identifier { if scalar == Scalar::ZERO { Err(FrostCoreError::InvalidZeroScalar) } else { - Ok(Self(scalar)) + Ok(Self { id, scalar }) } } + /// Return the raw participant ID. + pub fn to_u32(&self) -> u32 { + self.id + } + /// Return the underlying scalar. pub fn to_scalar(&self) -> Scalar { - self.0 + self.scalar } } impl PartialEq for Identifier { fn eq(&self, other: &Self) -> bool { - self.0 == other.0 + self.id == other.id } } @@ -69,18 +82,9 @@ impl PartialOrd for Identifier { // See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/identifier.rs#L121-L137 impl Ord for Identifier { - /// Compare identifiers by their numeric scalar value, using big-endian byte - /// order. Serializes to little-endian, and compares in reverse order. + /// Compare identifiers by their original participant ID. fn cmp(&self, other: &Self) -> Ordering { - let a = self.0.to_bytes(); - let b = other.0.to_bytes(); - for i in (0..32).rev() { - match a[i].cmp(&b[i]) { - Ordering::Equal => continue, - other => return other, - } - } - Ordering::Equal + self.id.cmp(&other.id) } } @@ -134,7 +138,7 @@ impl VerifiableSecretSharingCommitment { /// A secret scalar value representing a signer's share of the group secret. /// /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L82-L87 -#[derive(Copy, Clone, Debug)] +#[derive(Clone, Debug, ZeroizeOnDrop)] pub struct SigningShare(Scalar); impl SigningShare { @@ -153,6 +157,7 @@ impl SigningShare { Self::new(evaluate_polynomial(peer, coefficients)) } } + /// A public group element that represents a single signer's public /// verification share. /// @@ -242,6 +247,7 @@ impl SecretShare { /// Checks that `G * signing_share == evaluate_vss(identifier, commitment)`. /// /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L431-L468 + #[allow(clippy::arithmetic_side_effects)] pub fn verify(&self) -> Result<(), FrostCoreError> { let f_result = G1Projective::generator() * self.signing_share.to_scalar(); let result = evaluate_vss(self.identifier, &self.commitment); @@ -257,12 +263,16 @@ impl SecretShare { /// A key package containing all key material for a participant. /// /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L617-L643 -#[derive(Debug)] +#[derive(Debug, ZeroizeOnDrop)] pub struct KeyPackage { + #[zeroize(skip)] identifier: Identifier, signing_share: SigningShare, + #[zeroize(skip)] verifying_share: VerifyingShare, + #[zeroize(skip)] verifying_key: VerifyingKey, + #[zeroize(skip)] min_signers: u16, } @@ -378,6 +388,7 @@ impl PublicKeyPackage { /// `a_0 + a_1 * x + a_2 * x^2 + ... + a_{t-1} * x^{t-1}`. /// /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L573-L595 +#[allow(clippy::arithmetic_side_effects)] fn evaluate_polynomial(identifier: Identifier, coefficients: &[Scalar]) -> Scalar { let mut value = Scalar::ZERO; let x = identifier.to_scalar(); @@ -398,6 +409,7 @@ fn evaluate_polynomial(identifier: Identifier, coefficients: &[Scalar]) -> Scala /// Computes `sum_{k=0}^{t-1} commitment[k] * identifier^k`. /// /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L597-L615 +#[allow(clippy::arithmetic_side_effects)] fn evaluate_vss( identifier: Identifier, commitment: &VerifiableSecretSharingCommitment, @@ -420,6 +432,7 @@ fn evaluate_vss( /// elements across all participants. /// /// See: https://github.com/ZcashFoundation/frost/blob/3ffc19d8f473d5bc4e07ed41bc884bdb42d6c29f/frost-core/src/keys.rs#L35-L62 +#[allow(clippy::arithmetic_side_effects)] fn sum_commitments( commitments: &[&VerifiableSecretSharingCommitment], ) -> Result { @@ -461,3 +474,64 @@ pub fn validate_num_of_signers(min_signers: u16, max_signers: u16) -> Result<(), } Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn identifier_from_u32_rejects_zero() { + assert!(matches!( + Identifier::from_u32(0), + Err(FrostCoreError::InvalidZeroScalar) + )); + } + + #[test] + fn validate_num_of_signers_rejects_invalid_bounds() { + assert!(matches!( + validate_num_of_signers(1, 3), + Err(FrostCoreError::InvalidMinSigners) + )); + assert!(matches!( + validate_num_of_signers(2, 1), + Err(FrostCoreError::InvalidMaxSigners) + )); + assert!(matches!( + validate_num_of_signers(3, 2), + Err(FrostCoreError::InvalidMinSigners) + )); + } + + #[test] + fn secret_share_verify_rejects_invalid_share() { + let id = Identifier::from_u32(1).unwrap(); + let commitment = VerifiableSecretSharingCommitment::new(vec![CoefficientCommitment::new( + G1Projective::generator(), + )]); + let invalid_share = + SecretShare::new(id, SigningShare::new(Scalar::ZERO), commitment.clone()); + assert!(matches!( + invalid_share.verify(), + Err(FrostCoreError::InvalidSecretShare) + )); + } + + #[test] + fn verifying_key_from_commitment_rejects_empty_commitment() { + let empty_commitment = VerifiableSecretSharingCommitment::new(vec![]); + assert!(matches!( + VerifyingKey::from_commitment(&empty_commitment), + Err(FrostCoreError::IncorrectCommitment) + )); + } + + #[test] + fn public_key_package_from_dkg_commitments_rejects_empty_commitments() { + let empty_commitments = BTreeMap::new(); + assert!(matches!( + PublicKeyPackage::from_dkg_commitments(&empty_commitments), + Err(FrostCoreError::IncorrectNumberOfCommitments) + )); + } +} diff --git a/crates/frost/src/kryptology.rs b/crates/frost/src/kryptology.rs index 865a2414..08f94dba 100644 --- a/crates/frost/src/kryptology.rs +++ b/crates/frost/src/kryptology.rs @@ -9,56 +9,60 @@ //! The output types ([`KeyPackage`], [`PublicKeyPackage`]) are standard //! frost-core types usable with frost-core's signing protocol. -#![allow(clippy::arithmetic_side_effects)] - -use std::collections::BTreeMap; +use std::{collections::BTreeMap, fmt}; use blst::*; use rand_core::{CryptoRng, RngCore}; use sha2::{Digest, Sha256}; +use zeroize::ZeroizeOnDrop; use super::*; -/// Errors from the kryptology-compatible DKG. -#[derive(Debug)] -pub enum DkgError { +/// Errors from the kryptology-compatible FROST protocol. +#[derive(Debug, thiserror::Error)] +pub enum KryptologyError { /// Participant ID is zero or out of range. + #[error("invalid participant ID {0}")] InvalidParticipantId(u32), /// Two or more partial signatures share the same identifier. + #[error("duplicate participant identifier {0}")] DuplicateIdentifier(u32), /// Fewer partial signatures than the threshold were provided. + #[error("insufficient signers")] InsufficientSigners, /// Invalid number of signers. + #[error("invalid signer count")] InvalidSignerCount, /// Invalid proof of knowledge from a specific participant. + #[error("invalid proof from participant {culprit}")] InvalidProof { /// The 1-indexed ID of the participant whose proof failed. culprit: u32, }, /// Invalid Feldman share from a specific participant. + #[error("invalid share from participant {culprit}")] InvalidShare { /// The 1-indexed ID of the participant whose share failed. culprit: u32, }, /// Wrong number of received packages. + #[error("incorrect package count")] IncorrectPackageCount, /// Failed to deserialize a scalar from wire format bytes. + #[error("invalid scalar encoding")] InvalidScalar, /// Failed to deserialize a G1 point from wire format bytes. + #[error("invalid point encoding")] InvalidPoint, /// Commitment count does not match threshold. + #[error("invalid commitment count from participant {participant}")] InvalidCommitmentCount { /// The participant whose commitment count was wrong. participant: u32, }, /// An error from frost-core. - FrostCoreError(FrostCoreError), -} - -impl From for DkgError { - fn from(e: FrostCoreError) -> Self { - DkgError::FrostCoreError(e) - } + #[error(transparent)] + FrostCoreError(#[from] FrostCoreError), } /// Kryptology Round 1 broadcast data matching Go's `frost.Round1Bcast`. @@ -87,9 +91,10 @@ pub struct Round2Bcast { /// A Shamir secret share matching Go's `sharing.ShamirShare`. /// /// The `value` field is in **big-endian** byte order. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, ZeroizeOnDrop)] pub struct ShamirShare { /// The share identifier (1-indexed participant ID). + #[zeroize(skip)] pub id: u32, /// The share value as big-endian scalar bytes. pub value: [u8; 32], @@ -100,31 +105,39 @@ pub struct ShamirShare { /// # Security /// /// This MUST NOT be sent to other participants. +#[derive(ZeroizeOnDrop)] pub struct Round1Secret { + #[zeroize(skip)] id: u32, + #[zeroize(skip)] ctx: u8, coefficients: Vec, + #[zeroize(skip)] commitment: VerifiableSecretSharingCommitment, + #[zeroize(skip)] threshold: u16, + #[zeroize(skip)] max_signers: u16, } impl Round1Secret { - /// Reconstruct a [`Round1Secret`] from wire-format data (e.g. a test - /// fixture) so that the standard [`round2`] function can be called. + /// Reconstruct a [`Round1Secret`] from wire-format test fixture data so + /// that the standard [`round2`] function can be called. /// - /// `own_share` is the big-endian scalar the participant computed for - /// itself. It is stored as the constant term of a zero polynomial so - /// that [`round2`]'s `from_coefficients` evaluation returns it - /// unchanged. - pub fn from_raw( + /// Testing-only helper: `own_share` is stored as the constant term of a + /// synthetic zero polynomial so that [`round2`]'s `from_coefficients` + /// evaluation returns it unchanged. + #[cfg(test)] + pub(crate) fn from_raw( id: u32, ctx: u8, threshold: u16, max_signers: u16, own_share: &[u8; 32], commitment_bytes: &[[u8; 48]], - ) -> Result { + ) -> Result { + validate_round_parameters(id, threshold, max_signers)?; + let own_share_scalar = scalar_from_be(own_share)?; let commitment = deserialize_commitment(id, threshold, commitment_bytes)?; @@ -150,14 +163,15 @@ pub fn scalar_to_be(s: &Scalar) -> [u8; 32] { } /// Convert big-endian 32 bytes to a `Scalar`. -pub fn scalar_from_be(bytes: &[u8; 32]) -> Result { +pub fn scalar_from_be(bytes: &[u8; 32]) -> Result { let mut le = *bytes; le.reverse(); - Scalar::from_bytes(&le).ok_or(DkgError::InvalidScalar) + Scalar::from_bytes(&le).ok_or(KryptologyError::InvalidScalar) } /// RFC 9380 Section 5.3.1 using SHA-256 -pub fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { +#[allow(clippy::arithmetic_side_effects)] +fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { const B_IN_BYTES: usize = 32; // SHA-256 output const S_IN_BYTES: usize = 64; // SHA-256 block size @@ -215,6 +229,25 @@ pub fn expand_msg_xmd(msg: &[u8], dst: &[u8], len_in_bytes: usize) -> Vec { out } +fn validate_round_parameters( + id: u32, + threshold: u16, + max_signers: u16, +) -> Result<(), KryptologyError> { + // Kryptology encodes participant identifiers into a single byte. + if max_signers > u16::from(u8::MAX) { + return Err(KryptologyError::InvalidSignerCount); + } + + validate_num_of_signers(threshold, max_signers)?; + + if id == 0 || id > u32::from(max_signers) { + return Err(KryptologyError::InvalidParticipantId(id)); + } + + Ok(()) +} + /// Kryptology hash-to-scalar. /// /// See: https://github.com/coinbase/kryptology/blob/1dcc062313d99f2e56ce6abc2003ef63c52dd4a5/pkg/core/curves/bls12381_curve.go#L50 @@ -222,16 +255,10 @@ const KRYPTOLOGY_DST: &[u8] = b"BLS12381_XMD:SHA-256_SSWU_RO_"; /// Hash to scalar using kryptology's ExpandMsgXmd construction. /// -/// `ExpandMsgXmd(SHA-256, msg, DST, 48)` -> reverse bytes -> pad to 64 -> -/// `Scalar::from_bytes_wide`. +/// `ExpandMsgXmd(SHA-256, msg, DST, 48)` -> `Scalar::from_be_bytes_wide`. fn kryptology_hash_to_scalar(msg: &[u8]) -> Scalar { let xmd = expand_msg_xmd(msg, KRYPTOLOGY_DST, 48); - let mut reversed = [0u8; 48]; - reversed.copy_from_slice(&xmd); - reversed.reverse(); - let mut wide = [0u8; 64]; - wide[..48].copy_from_slice(&reversed); - Scalar::from_bytes_wide(&wide) + Scalar::from_be_bytes_wide(&xmd) } /// Compute the DKG challenge matching kryptology's format. @@ -251,12 +278,13 @@ fn deserialize_commitment( participant: u32, threshold: u16, commitments: &[[u8; 48]], -) -> Result { +) -> Result { if commitments.len() != threshold as usize { - return Err(DkgError::InvalidCommitmentCount { participant }); + return Err(KryptologyError::InvalidCommitmentCount { participant }); } - VerifiableSecretSharingCommitment::from_commitments(commitments).ok_or(DkgError::InvalidPoint) + VerifiableSecretSharingCommitment::from_commitments(commitments) + .ok_or(KryptologyError::InvalidPoint) } /// Perform Round 1 of the kryptology-compatible DKG. @@ -271,23 +299,15 @@ fn deserialize_commitment( /// - `max_signers`: Total number of signers (n). /// - `ctx`: DKG context byte (typically 0). /// - `rng`: Cryptographic RNG. +#[allow(clippy::arithmetic_side_effects)] pub fn round1( id: u32, threshold: u16, max_signers: u16, ctx: u8, rng: &mut R, -) -> Result<(Round1Bcast, BTreeMap, Round1Secret), DkgError> { - // Kryptology encodes participant identifiers into a single byte. - if max_signers > u16::from(u8::MAX) { - return Err(DkgError::InvalidSignerCount); - } - - validate_num_of_signers(threshold, max_signers)?; - - if id == 0 || id > u32::from(max_signers) { - return Err(DkgError::InvalidParticipantId(id)); - } +) -> Result<(Round1Bcast, BTreeMap, Round1Secret), KryptologyError> { + validate_round_parameters(id, threshold, max_signers)?; // Generate random polynomial coefficients [a_0, ..., a_{t-1}] let coefficients: Vec = (0..threshold).map(|_| Scalar::random(&mut *rng)).collect(); @@ -368,14 +388,20 @@ pub fn round1( /// [`Round1Bcast`]. /// - `received_shares`: Map from source participant ID to the [`ShamirShare`] /// they sent us. +#[allow(clippy::arithmetic_side_effects)] pub fn round2( secret: Round1Secret, received_bcasts: &BTreeMap, received_shares: &BTreeMap, -) -> Result<(Round2Bcast, KeyPackage, PublicKeyPackage), DkgError> { - let expected = (secret.max_signers - 1) as usize; - if received_bcasts.len() != expected || received_shares.len() != expected { - return Err(DkgError::IncorrectPackageCount); +) -> Result<(Round2Bcast, KeyPackage, PublicKeyPackage), KryptologyError> { + let min_received = (secret.threshold - 1) as usize; + let max_received = (secret.max_signers - 1) as usize; + if received_bcasts.len() < min_received + || received_bcasts.len() > max_received + || received_shares.len() < min_received + || received_shares.len() > max_received + { + return Err(KryptologyError::IncorrectPackageCount); } let own_identifier = Identifier::from_u32(secret.id)?; @@ -387,6 +413,10 @@ pub fn round2( let mut share_sum = Scalar::ZERO; for (&sender_id, bcast) in received_bcasts { + if sender_id == secret.id { + return Err(KryptologyError::InvalidParticipantId(sender_id)); + } + let sender_commitment = deserialize_commitment(sender_id, secret.threshold, &bcast.commitments)?; let a0 = sender_commitment.coefficients()[0].value(); @@ -394,22 +424,25 @@ pub fn round2( // Verify proof of knowledge let wi = scalar_from_be(&bcast.wi)?; let ci = scalar_from_be(&bcast.ci)?; + if ci == Scalar::ZERO { + return Err(KryptologyError::InvalidProof { culprit: sender_id }); + } // Reconstruct R' = Wi*G - Ci*A_{j,0} let r_reconstructed = G1Projective::generator() * wi - a0 * ci; - let sender_id_u8 = - u8::try_from(sender_id).map_err(|_| DkgError::InvalidParticipantId(sender_id))?; + let sender_id_u8 = u8::try_from(sender_id) + .map_err(|_| KryptologyError::InvalidParticipantId(sender_id))?; let ci_check = kryptology_challenge(sender_id_u8, secret.ctx, &a0, &r_reconstructed); - if ci_check != ci { - return Err(DkgError::InvalidProof { culprit: sender_id }); + if !ci_check.constant_time_eq(&ci) { + return Err(KryptologyError::InvalidProof { culprit: sender_id }); } // Verify Feldman share let share = received_shares .get(&sender_id) - .ok_or(DkgError::IncorrectPackageCount)?; + .ok_or(KryptologyError::InvalidShare { culprit: sender_id })?; if share.id != secret.id { - return Err(DkgError::InvalidShare { culprit: sender_id }); + return Err(KryptologyError::InvalidShare { culprit: sender_id }); } let share_scalar = scalar_from_be(&share.value)?; @@ -418,7 +451,7 @@ pub fn round2( SecretShare::new(own_identifier, signing_share, sender_commitment.clone()); secret_share .verify() - .map_err(|_| DkgError::InvalidShare { culprit: sender_id })?; + .map_err(|_| KryptologyError::InvalidShare { culprit: sender_id })?; share_sum = share_sum + share_scalar; @@ -433,7 +466,7 @@ pub fn round2( let verifying_share = VerifyingShare::new(verifying_share_element); // Build PublicKeyPackage from all participants' commitments - peer_commitments.insert(own_identifier, secret.commitment); + peer_commitments.insert(own_identifier, secret.commitment.clone()); let commitment_refs: BTreeMap = peer_commitments.iter().map(|(id, c)| (*id, c)).collect(); let public_key_package = PublicKeyPackage::from_dkg_commitments(&commitment_refs)?; @@ -472,23 +505,34 @@ pub struct BlsPartialSignature { point: blst_p2, } +impl fmt::Debug for BlsPartialSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut affine = blst_p2_affine::default(); + let mut bytes = [0u8; 96]; + unsafe { + blst_p2_to_affine(&mut affine, &self.point); + blst_p2_affine_compress(bytes.as_mut_ptr(), &affine); + } + + f.debug_struct("BlsPartialSignature") + .field("identifier", &self.identifier) + .field("point", &bytes) + .finish() + } +} + impl BlsPartialSignature { /// Produce a BLS partial signature from a [`KeyPackage`] produced by /// kryptology DKG. /// /// Computes `partial_sig = (key_package.signing_share) * H(msg)` where H /// hashes the message to a G2 point using the Ethereum 2.0 DST. - /// - /// The `id` must be the original 1-indexed kryptology participant ID. - pub fn from_key_package(id: u32, key_package: &KeyPackage, msg: &[u8]) -> BlsPartialSignature { + pub fn from_key_package(key_package: &KeyPackage, msg: &[u8]) -> BlsPartialSignature { let scalar = key_package.signing_share().to_scalar(); - { - let signing_share: &Scalar = &scalar; - let h_msg = hash_to_g2(msg); - BlsPartialSignature { - identifier: id, - point: p2_mult(&h_msg, signing_share), - } + let h_msg = hash_to_g2(msg); + BlsPartialSignature { + identifier: key_package.identifier().to_u32(), + point: p2_mult(&h_msg, &scalar), } } } @@ -499,6 +543,14 @@ pub struct BlsSignature { point: blst_p2, } +impl fmt::Debug for BlsSignature { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_tuple("BlsSignature") + .field(&self.to_bytes()) + .finish() + } +} + impl BlsSignature { /// Serialize to 96-byte compressed G2 point. pub fn to_bytes(&self) -> [u8; 96] { @@ -516,21 +568,22 @@ impl BlsSignature { /// Matches Go's `combineSigs` in /// `kryptology/pkg/signatures/bls/bls_sig/usual_bls_sig.go`. /// - /// Returns [`DkgError::InsufficientSigners`] if `min_signers < 2` or + /// Returns [`KryptologyError::InsufficientSigners`] if `min_signers < 2` or /// fewer than `min_signers` partial signatures are provided. + #[allow(clippy::arithmetic_side_effects)] pub fn from_partial_signatures( min_signers: u16, partial_sigs: &[BlsPartialSignature], - ) -> Result { + ) -> Result { if min_signers < 2 || partial_sigs.len() < min_signers as usize { - return Err(DkgError::InsufficientSigners); + return Err(KryptologyError::InsufficientSigners); } // Check for duplicate identifiers let mut seen = std::collections::BTreeSet::new(); for ps in partial_sigs { if !seen.insert(ps.identifier) { - return Err(DkgError::DuplicateIdentifier(ps.identifier)); + return Err(KryptologyError::DuplicateIdentifier(ps.identifier)); } } @@ -540,7 +593,6 @@ impl BlsSignature { .collect(); let mut combined = blst_p2::default(); - let mut first = true; for (i, ps) in partial_sigs.iter().enumerate() { // Lagrange coefficient: L_i(0) = prod_{j!=i} ( x_j / (x_j - x_i) ) @@ -551,20 +603,17 @@ impl BlsSignature { } let num = x_vals[j]; let den = x_vals[j] - x_vals[i]; - let den_inv = den.invert().ok_or(DkgError::InvalidSignerCount)?; + // Duplicate identifiers are rejected above, so this should + // only fail if the invariant is broken. + let den_inv = den.invert().ok_or(KryptologyError::InvalidSignerCount)?; lambda = lambda * num * den_inv; } let weighted = p2_mult(&ps.point, &lambda); - if first { - combined = weighted; - first = false; - } else { - let mut tmp = blst_p2::default(); - unsafe { blst_p2_add_or_double(&mut tmp, &combined, &weighted) }; - combined = tmp; - } + let mut tmp = blst_p2::default(); + unsafe { blst_p2_add_or_double(&mut tmp, &combined, &weighted) }; + combined = tmp; } Ok(BlsSignature { point: combined }) @@ -609,7 +658,482 @@ fn p2_mult(point: &blst_p2, scalar: &Scalar) -> blst_p2 { let mut out = blst_p2::default(); unsafe { blst_scalar_from_fr(&mut s, &scalar.0); + // BLS12-381 scalar field order has 255 significant bits. blst_p2_mult(&mut out, point, s.b.as_ptr(), 255); } out } + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use rand::{SeedableRng, rngs::StdRng}; + + use super::*; + + #[test] + fn scalar_from_be_rejects_invalid_scalar_encoding() { + assert!(matches!( + scalar_from_be(&[0xff; 32]), + Err(KryptologyError::InvalidScalar) + )); + } + + #[test] + fn deserialize_commitment_rejects_wrong_commitment_count() { + let commitments = []; + + assert!(matches!( + deserialize_commitment(2, 1, &commitments), + Err(KryptologyError::InvalidCommitmentCount { participant: 2 }) + )); + } + + #[test] + fn deserialize_commitment_rejects_invalid_point() { + let commitments = [[0u8; 48]]; + + assert!(matches!( + deserialize_commitment(2, 1, &commitments), + Err(KryptologyError::InvalidPoint) + )); + } + + #[test] + fn round2_rejects_insufficient_package_count() { + let mut rng = StdRng::seed_from_u64(11); + let (_bcast, _shares, secret) = round1(1, 2, 3, 0, &mut rng).unwrap(); + + assert!(matches!( + round2(secret, &BTreeMap::new(), &BTreeMap::new()), + Err(KryptologyError::IncorrectPackageCount) + )); + } + + #[test] + fn from_partial_signatures_rejects_insufficient_signers() { + assert!(matches!( + BlsSignature::from_partial_signatures(2, &[]), + Err(KryptologyError::InsufficientSigners) + )); + } + + /// RFC 9380 Section 5.3.1 test vector for expand_msg_xmd with SHA-256. + /// DST = "QUUX-V01-CS02-with-expander-SHA256-128" + /// msg = "" (empty), len_in_bytes = 0x20 (32) + #[test] + fn expand_msg_xmd_rfc9380_vector() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; + let msg = b""; + let expected = + hex::decode("68a985b87eb6b46952128911f2a4412bbc302a9d759667f87f7a21d803f07235") + .unwrap(); + + let result = expand_msg_xmd(msg, dst, 32); + assert_eq!(result, expected, "expand_msg_xmd empty message vector"); + } + + /// RFC 9380 test vector: msg = "abc", len = 32 + #[test] + fn expand_msg_xmd_rfc9380_abc() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; + let msg = b"abc"; + let expected = + hex::decode("d8ccab23b5985ccea865c6c97b6e5b8350e794e603b4b97902f53a8a0d605615") + .unwrap(); + + let result = expand_msg_xmd(msg, dst, 32); + assert_eq!(result, expected, "expand_msg_xmd abc vector"); + } + + /// RFC 9380 test vector: msg = "", len = 0x80 (128 bytes) + #[test] + fn expand_msg_xmd_rfc9380_long_output() { + let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; + let msg = b""; + let expected = hex::decode( + "af84c27ccfd45d41914fdff5df25293e221afc53d8ad2ac06d5e3e2948\ + 5dadbee0d121587713a3e0dd4d5e69e93eb7cd4f5df4cd103e188cf60c\ + b02edc3edf18eda8576c412b18ffb658e3dd6ec849469b979d444cf7b2\ + 6911a08e63cf31f9dcc541708d3491184472c2c29bb749d4286b004ceb\ + 5ee6b9a7fa5b646c993f0ced", + ) + .unwrap(); + + let result = expand_msg_xmd(msg, dst, 128); + assert_eq!(result, expected, "expand_msg_xmd 128-byte output vector"); + } + + #[test] + fn round1_rejects_more_than_255_signers() { + let mut rng = StdRng::seed_from_u64(42); + let result = round1(1, 2, 256, 0, &mut rng); + + assert!(matches!(result, Err(KryptologyError::InvalidSignerCount))); + } + + #[test] + fn round1_accepts_255_signers_boundary() { + let mut rng = StdRng::seed_from_u64(4242); + let (_bcast, shares, _secret) = round1(1, 2, 255, 9, &mut rng) + .expect("255 signers should remain within kryptology's u8 transport limit"); + + assert_eq!(shares.len(), 254); + assert!(shares.contains_key(&255)); + } + + #[test] + fn round1_rejects_invalid_signer_counts() { + let mut rng = StdRng::seed_from_u64(7); + + assert!(matches!( + round1(1, 1, 3, 0, &mut rng), + Err(KryptologyError::FrostCoreError( + FrostCoreError::InvalidMinSigners + )) + )); + assert!(matches!( + round1(1, 3, 2, 0, &mut rng), + Err(KryptologyError::FrostCoreError( + FrostCoreError::InvalidMinSigners + )) + )); + assert!(matches!( + round1(0, 2, 3, 0, &mut rng), + Err(KryptologyError::InvalidParticipantId(0)) + )); + } + + /// Full DKG round-trip: 3-of-3 DKG, then BLS threshold sign and verify. + #[test] + fn bls_round_trip_3_of_3() { + let mut rng = StdRng::seed_from_u64(42); + let threshold = 3u16; + let max_signers = 3u16; + let ctx = 0u8; + + let mut bcasts: BTreeMap = BTreeMap::new(); + let mut all_shares: BTreeMap> = BTreeMap::new(); + let mut secrets: BTreeMap = BTreeMap::new(); + + for id in 1..=u32::from(max_signers) { + let (bcast, shares, secret) = + round1(id, threshold, max_signers, ctx, &mut rng).expect("round1 should succeed"); + bcasts.insert(id, bcast); + secrets.insert(id, secret); + + for (&target_id, share) in &shares { + all_shares + .entry(target_id) + .or_default() + .insert(id, share.clone()); + } + } + + let mut key_packages = BTreeMap::new(); + let mut public_key_packages = Vec::new(); + let mut round2_bcasts = BTreeMap::new(); + + for id in 1..=u32::from(max_signers) { + let received_bcasts: BTreeMap = bcasts + .iter() + .filter(|(k, _)| **k != id) + .map(|(k, v)| (*k, v.clone())) + .collect(); + + let received_shares = all_shares.remove(&id).unwrap(); + let secret = secrets.remove(&id).unwrap(); + + let (r2_bcast, key_package, pub_package) = + round2(secret, &received_bcasts, &received_shares).expect("round2 should succeed"); + + round2_bcasts.insert(id, r2_bcast); + key_packages.insert(id, key_package); + public_key_packages.push(pub_package); + } + + let vk = public_key_packages[0].verifying_key(); + for pkg in &public_key_packages[1..] { + assert_eq!( + vk, + pkg.verifying_key(), + "all participants must agree on the group key" + ); + } + + let vk_bytes = round2_bcasts[&1].verification_key; + for (&id, bcast) in &round2_bcasts { + assert_eq!( + bcast.verification_key, vk_bytes, + "participant {id} round2 broadcast has different group key" + ); + } + + let message = b"test message"; + + let partial_sigs: Vec<_> = key_packages + .keys() + .map(|&id| BlsPartialSignature::from_key_package(&key_packages[&id], message)) + .collect(); + + let signature = BlsSignature::from_partial_signatures(threshold, &partial_sigs) + .expect("BLS signature combination should succeed"); + + assert!( + signature.verify(vk, message), + "3-of-3 BLS threshold signature should verify" + ); + } + + /// 2-of-3 DKG then BLS threshold signing (Ethereum 2.0 compatible). + #[test] + fn bls_round_trip_2_of_3() { + let mut rng = StdRng::seed_from_u64(123); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let mut bcasts: BTreeMap = BTreeMap::new(); + let mut all_shares: BTreeMap> = BTreeMap::new(); + let mut secrets: BTreeMap = BTreeMap::new(); + + for id in 1..=u32::from(max_signers) { + let (bcast, shares, secret) = + round1(id, threshold, max_signers, ctx, &mut rng).unwrap(); + bcasts.insert(id, bcast); + secrets.insert(id, secret); + for (&target_id, share) in &shares { + all_shares + .entry(target_id) + .or_default() + .insert(id, share.clone()); + } + } + + let mut key_packages = BTreeMap::new(); + let mut public_key_packages = Vec::new(); + + for id in 1..=u32::from(max_signers) { + let received_bcasts: BTreeMap<_, _> = bcasts + .iter() + .filter(|(k, _)| **k != id) + .map(|(k, v)| (*k, v.clone())) + .collect(); + let received_shares = all_shares.remove(&id).unwrap(); + let secret = secrets.remove(&id).unwrap(); + + let (_r2_bcast, key_package, pub_package) = + round2(secret, &received_bcasts, &received_shares).unwrap(); + key_packages.insert(id, key_package); + public_key_packages.push(pub_package); + } + + let message = b"threshold signing"; + let signers: [u32; 2] = [1, 2]; + + let partial_sigs: Vec<_> = signers + .iter() + .map(|&id| BlsPartialSignature::from_key_package(&key_packages[&id], message)) + .collect(); + + let signature = BlsSignature::from_partial_signatures(threshold, &partial_sigs) + .expect("BLS signature combination should succeed"); + + let vk = public_key_packages[0].verifying_key(); + assert!( + signature.verify(vk, message), + "BLS threshold signature should verify" + ); + let signature_bytes = signature.to_bytes(); + let parsed_signature = blst::min_pk::Signature::from_bytes(&signature_bytes) + .expect("combined signature should serialize to compressed bytes"); + assert_eq!(parsed_signature.to_bytes(), signature_bytes); + + assert!( + !signature.verify(vk, b"wrong message"), + "BLS signature should not verify against a different message" + ); + } + + /// Verify that an invalid proof is caught in round2. + #[test] + fn round2_rejects_invalid_proof() { + let mut rng = StdRng::seed_from_u64(99); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (mut bcast1, shares1, _secret1) = + round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast2, _shares2, secret2) = + round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + let (bcast3, shares3, _secret3) = round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); + + bcast1.ci[31] ^= 0x01; + + let received_bcasts: BTreeMap = + [(1, bcast1.clone()), (3, bcast3.clone())].into(); + let received_shares: BTreeMap = + [(1, shares1[&2].clone()), (3, shares3[&2].clone())].into(); + + let result = round2(secret2, &received_bcasts, &received_shares); + assert!(result.is_err()); + match result.unwrap_err() { + KryptologyError::InvalidProof { culprit } => assert_eq!(culprit, 1), + other => panic!("expected InvalidProof, got {other:?}"), + } + } + + #[test] + fn round2_rejects_zero_challenge() { + let mut rng = StdRng::seed_from_u64(98); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (mut bcast1, shares1, _secret1) = + round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast2, _shares2, secret2) = + round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + + bcast1.ci = [0; 32]; + + let result = round2( + secret2, + &[(1, bcast1)].into(), + &[(1, shares1[&2].clone())].into(), + ); + + assert!(matches!( + result, + Err(KryptologyError::InvalidProof { culprit: 1 }) + )); + } + + /// Verify that a share addressed to the wrong participant is rejected in + /// round2. + #[test] + fn round2_rejects_share_id_mismatch() { + let mut rng = StdRng::seed_from_u64(42); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (bcast1, shares1, _secret1) = round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast2, _shares2, secret2) = + round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + let (bcast3, shares3, _secret3) = round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); + + let received_bcasts: BTreeMap = [(1, bcast1), (3, bcast3)].into(); + + let mut wrong_share = shares1[&2].clone(); + wrong_share.id = 3; + let received_shares: BTreeMap = + [(1, wrong_share), (3, shares3[&2].clone())].into(); + + let result = round2(secret2, &received_bcasts, &received_shares); + assert!(result.is_err()); + match result.unwrap_err() { + KryptologyError::InvalidShare { culprit } => assert_eq!(culprit, 1), + other => panic!("expected InvalidShare, got {other:?}"), + } + } + + #[test] + fn round2_accepts_threshold_subset() { + let mut rng = StdRng::seed_from_u64(321); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (bcast1, shares1, _secret1) = round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast2, _shares2, secret2) = + round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast3, _shares3, _secret3) = + round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); + + let received_bcasts: BTreeMap = [(1, bcast1)].into(); + let received_shares: BTreeMap = [(1, shares1[&2].clone())].into(); + + round2(secret2, &received_bcasts, &received_shares) + .expect("threshold-1 peer packages should be enough"); + } + + #[test] + fn round2_rejects_missing_share_with_culprit() { + let mut rng = StdRng::seed_from_u64(322); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (bcast1, _shares1, _secret1) = + round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (_bcast2, _shares2, secret2) = + round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + let (bcast3, shares3, _secret3) = round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); + + let received_bcasts: BTreeMap = [(1, bcast1), (3, bcast3)].into(); + let received_shares: BTreeMap = [(3, shares3[&2].clone())].into(); + + let result = round2(secret2, &received_bcasts, &received_shares); + assert!(matches!( + result, + Err(KryptologyError::InvalidShare { culprit: 1 }) + )); + } + + #[test] + fn round2_rejects_self_broadcast() { + let mut rng = StdRng::seed_from_u64(323); + let threshold = 2u16; + let max_signers = 3u16; + let ctx = 0u8; + + let (_bcast1, shares1, _secret1) = + round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (bcast2, _shares2, secret2) = round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + + let received_bcasts: BTreeMap = [(2, bcast2)].into(); + let received_shares: BTreeMap = [(2, shares1[&2].clone())].into(); + + let result = round2(secret2, &received_bcasts, &received_shares); + assert!(matches!( + result, + Err(KryptologyError::InvalidParticipantId(2)) + )); + } + + #[test] + fn from_partial_signatures_rejects_duplicate_signers() { + let mut rng = StdRng::seed_from_u64(324); + let threshold = 2u16; + let max_signers = 2u16; + let ctx = 0u8; + let message = b"duplicate signer"; + + let (bcast1, shares1, secret1) = round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); + let (bcast2, shares2, secret2) = round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); + + let (_round2_bcast1, key_package1, _public_key_package1) = round2( + secret1, + &[(2, bcast2.clone())].into(), + &[(2, shares2[&1].clone())].into(), + ) + .unwrap(); + let (_round2_bcast2, _key_package2, _public_key_package2) = round2( + secret2, + &[(1, bcast1)].into(), + &[(1, shares1[&2].clone())].into(), + ) + .unwrap(); + + let partial = BlsPartialSignature::from_key_package(&key_package1, message); + let result = BlsSignature::from_partial_signatures(threshold, &[partial.clone(), partial]); + + assert!(matches!( + result, + Err(KryptologyError::DuplicateIdentifier(1)) + )); + } +} diff --git a/crates/frost/tests/kryptology_interop.rs b/crates/frost/src/kryptology_interop_tests.rs similarity index 88% rename from crates/frost/tests/kryptology_interop.rs rename to crates/frost/src/kryptology_interop_tests.rs index 432e8ebd..d62a30a3 100644 --- a/crates/frost/tests/kryptology_interop.rs +++ b/crates/frost/src/kryptology_interop_tests.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; -use pluto_frost::kryptology; +use crate::kryptology; use serde::Deserialize; #[derive(Clone, Deserialize)] @@ -73,7 +73,7 @@ impl From<&FixtureRound1Bcast> for kryptology::Round1Bcast { #[test] fn kryptology_fixture_round2_interop_2_of_3_ctx_0() { replay_fixture( - include_str!("./kryptology_fixtures/2-of-3-ctx-0.json"), + include_str!("../tests/kryptology_fixtures/2-of-3-ctx-0.json"), true, ); } @@ -81,7 +81,7 @@ fn kryptology_fixture_round2_interop_2_of_3_ctx_0() { #[test] fn kryptology_fixture_round2_interop_3_of_3_ctx_0() { replay_fixture( - include_str!("./kryptology_fixtures/3-of-3-ctx-0.json"), + include_str!("../tests/kryptology_fixtures/3-of-3-ctx-0.json"), true, ); } @@ -89,7 +89,7 @@ fn kryptology_fixture_round2_interop_3_of_3_ctx_0() { #[test] fn kryptology_fixture_round2_interop_malformed_share_id() { replay_fixture( - include_str!("./kryptology_fixtures/malformed-share-id.json"), + include_str!("../tests/kryptology_fixtures/malformed-share-id.json"), false, ); } @@ -97,7 +97,7 @@ fn kryptology_fixture_round2_interop_malformed_share_id() { #[test] fn kryptology_fixture_round2_interop_invalid_proof() { replay_fixture( - include_str!("./kryptology_fixtures/invalid-proof.json"), + include_str!("../tests/kryptology_fixtures/invalid-proof.json"), false, ); } @@ -174,14 +174,14 @@ fn replay_fixture(json: &str, require_group_signature: bool) { ExpectedRound2::InvalidShare { culprit } => { let err = result.expect_err("round2 should fail"); assert!( - matches!(err, kryptology::DkgError::InvalidShare { culprit: c } if c == *culprit), + matches!(err, kryptology::KryptologyError::InvalidShare { culprit: c } if c == *culprit), "expected InvalidShare(culprit={culprit}), got {err:?}" ); } ExpectedRound2::InvalidProof { culprit } => { let err = result.expect_err("round2 should fail"); assert!( - matches!(err, kryptology::DkgError::InvalidProof { culprit: c } if c == *culprit), + matches!(err, kryptology::KryptologyError::InvalidProof { culprit: c } if c == *culprit), "expected InvalidProof(culprit={culprit}), got {err:?}" ); } @@ -189,6 +189,9 @@ fn replay_fixture(json: &str, require_group_signature: bool) { } if !require_group_signature { + // Error fixtures assert each participant's expected round2 outcome + // above; they intentionally do not produce enough key packages for a + // group signature check. return; } @@ -200,8 +203,8 @@ fn replay_fixture(json: &str, require_group_signature: bool) { let message = b"kryptology fixture signing"; let partial_sigs: Vec<_> = key_packages - .iter() - .map(|(&id, kp)| kryptology::BlsPartialSignature::from_key_package(id, kp, message)) + .values() + .map(|kp| kryptology::BlsPartialSignature::from_key_package(kp, message)) .collect(); let signature = diff --git a/crates/frost/tests/kryptology_round_trip.rs b/crates/frost/src/kryptology_round_trip_tests.rs similarity index 96% rename from crates/frost/tests/kryptology_round_trip.rs rename to crates/frost/src/kryptology_round_trip_tests.rs index e0335f46..bf994eed 100644 --- a/crates/frost/tests/kryptology_round_trip.rs +++ b/crates/frost/src/kryptology_round_trip_tests.rs @@ -2,9 +2,10 @@ use std::collections::BTreeMap; -use pluto_frost::kryptology; use rand::{SeedableRng, rngs::StdRng}; +use crate::kryptology; + /// FROST DKG + BLS threshold signing (Ethereum 2.0 compatible). /// This matches Go's signing flow: non-interactive BLS partial signatures /// combined via Lagrange interpolation, verified with standard BLS pairings. @@ -96,9 +97,7 @@ fn kryptology_bls_round_trip_2_of_4_ctx_0() { let partial_sigs: Vec<_> = signing_participants .iter() - .map(|&id| { - kryptology::BlsPartialSignature::from_key_package(id, &key_packages[&id], message) - }) + .map(|&id| kryptology::BlsPartialSignature::from_key_package(&key_packages[&id], message)) .collect(); assert_eq!(partial_sigs.len(), threshold as usize); diff --git a/crates/frost/src/lib.rs b/crates/frost/src/lib.rs index 1197bf98..0884466d 100644 --- a/crates/frost/src/lib.rs +++ b/crates/frost/src/lib.rs @@ -3,7 +3,6 @@ //! Go's Coinbase Kryptology FROST DKG, and BLS threshold signing (Ethereum 2.0 //! compatible). -#![allow(non_snake_case)] #![doc = include_str!("../dkg.md")] pub mod curve; @@ -12,7 +11,9 @@ pub mod kryptology; pub use curve::*; pub use frost_core::*; -pub use rand_core; +pub use rand_core::{CryptoRng, RngCore}; #[cfg(test)] -mod tests; +mod kryptology_interop_tests; +#[cfg(test)] +mod kryptology_round_trip_tests; diff --git a/crates/frost/src/tests.rs b/crates/frost/src/tests.rs deleted file mode 100644 index 8e8013a2..00000000 --- a/crates/frost/src/tests.rs +++ /dev/null @@ -1,301 +0,0 @@ -use std::collections::BTreeMap; - -use rand::{SeedableRng, rngs::StdRng}; - -use crate::kryptology; - -#[test] -fn scalar_one_precomputed() { - let constant = crate::Scalar::ONE; - let computed = crate::Scalar::from(1u64); - assert_eq!(constant, computed); -} - -/// RFC 9380 Section 5.3.1 test vector for expand_msg_xmd with SHA-256. -/// DST = "QUUX-V01-CS02-with-expander-SHA256-128" -/// msg = "" (empty), len_in_bytes = 0x20 (32) -#[test] -fn expand_msg_xmd_rfc9380_vector() { - let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; - let msg = b""; - let expected = - hex::decode("68a985b87eb6b46952128911f2a4412bbc302a9d759667f87f7a21d803f07235").unwrap(); - - let result = kryptology::expand_msg_xmd(msg, dst, 32); - assert_eq!(result, expected, "expand_msg_xmd empty message vector"); -} - -/// RFC 9380 test vector: msg = "abc", len = 32 -#[test] -fn expand_msg_xmd_rfc9380_abc() { - let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; - let msg = b"abc"; - let expected = - hex::decode("d8ccab23b5985ccea865c6c97b6e5b8350e794e603b4b97902f53a8a0d605615").unwrap(); - - let result = kryptology::expand_msg_xmd(msg, dst, 32); - assert_eq!(result, expected, "expand_msg_xmd abc vector"); -} - -/// RFC 9380 test vector: msg = "", len = 0x80 (128 bytes) -#[test] -fn expand_msg_xmd_rfc9380_long_output() { - let dst = b"QUUX-V01-CS02-with-expander-SHA256-128"; - let msg = b""; - let expected = hex::decode( - "af84c27ccfd45d41914fdff5df25293e221afc53d8ad2ac06d5e3e2948\ - 5dadbee0d121587713a3e0dd4d5e69e93eb7cd4f5df4cd103e188cf60c\ - b02edc3edf18eda8576c412b18ffb658e3dd6ec849469b979d444cf7b2\ - 6911a08e63cf31f9dcc541708d3491184472c2c29bb749d4286b004ceb\ - 5ee6b9a7fa5b646c993f0ced", - ) - .unwrap(); - - let result = kryptology::expand_msg_xmd(msg, dst, 128); - assert_eq!(result, expected, "expand_msg_xmd 128-byte output vector"); -} - -#[test] -fn kryptology_rejects_more_than_255_signers() { - let mut rng = StdRng::seed_from_u64(42); - let result = kryptology::round1(1, 2, 256, 0, &mut rng); - - assert!(matches!( - result, - Err(kryptology::DkgError::InvalidSignerCount) - )); -} - -#[test] -fn kryptology_accepts_255_signers_boundary() { - let mut rng = StdRng::seed_from_u64(4242); - let (_bcast, shares, _secret) = kryptology::round1(1, 2, 255, 9, &mut rng) - .expect("255 signers should remain within kryptology's u8 transport limit"); - - assert_eq!(shares.len(), 254); - assert!(shares.contains_key(&255)); -} - -/// Full DKG round-trip: 3-of-3 DKG, then BLS threshold sign and verify. -#[test] -fn kryptology_bls_round_trip_3_of_3() { - let mut rng = StdRng::seed_from_u64(42); - let threshold = 3u16; - let max_signers = 3u16; - let ctx = 0u8; - - let mut bcasts: BTreeMap = BTreeMap::new(); - let mut all_shares: BTreeMap> = BTreeMap::new(); - let mut secrets: BTreeMap = BTreeMap::new(); - - for id in 1..=u32::from(max_signers) { - let (bcast, shares, secret) = kryptology::round1(id, threshold, max_signers, ctx, &mut rng) - .expect("round1 should succeed"); - bcasts.insert(id, bcast); - secrets.insert(id, secret); - - for (&target_id, share) in &shares { - all_shares - .entry(target_id) - .or_default() - .insert(id, share.clone()); - } - } - - // --- Round 2: each participant verifies + aggregates --- - let mut key_packages = BTreeMap::new(); - let mut public_key_packages = Vec::new(); - let mut round2_bcasts = BTreeMap::new(); - - for id in 1..=u32::from(max_signers) { - // Collect broadcasts from everyone except ourselves - let received_bcasts: BTreeMap = bcasts - .iter() - .filter(|(k, _)| **k != id) - .map(|(k, v)| (*k, v.clone())) - .collect(); - - let received_shares = all_shares.remove(&id).unwrap(); - let secret = secrets.remove(&id).unwrap(); - - let (r2_bcast, key_package, pub_package) = - kryptology::round2(secret, &received_bcasts, &received_shares) - .expect("round2 should succeed"); - - round2_bcasts.insert(id, r2_bcast); - key_packages.insert(id, key_package); - public_key_packages.push(pub_package); - } - - // All participants should agree on the group verification key - let vk = public_key_packages[0].verifying_key(); - for pkg in &public_key_packages[1..] { - assert_eq!( - vk, - pkg.verifying_key(), - "all participants must agree on the group key" - ); - } - - // All Round2Bcast should carry the same verification_key - let vk_bytes = round2_bcasts[&1].verification_key; - for (&id, bcast) in &round2_bcasts { - assert_eq!( - bcast.verification_key, vk_bytes, - "participant {id} round2 broadcast has different group key" - ); - } - - // BLS sign with all signers (t-of-t) - let message = b"test message"; - - let partial_sigs: Vec<_> = key_packages - .keys() - .map(|&id| { - kryptology::BlsPartialSignature::from_key_package(id, &key_packages[&id], message) - }) - .collect(); - - let signature = kryptology::BlsSignature::from_partial_signatures(threshold, &partial_sigs) - .expect("BLS signature combination should succeed"); - - assert!( - signature.verify(vk, message), - "3-of-3 BLS threshold signature should verify" - ); -} - -/// 2-of-3 DKG then BLS threshold signing (Ethereum 2.0 compatible). -#[test] -fn kryptology_bls_round_trip_2_of_3() { - let mut rng = StdRng::seed_from_u64(123); - let threshold = 2u16; - let max_signers = 3u16; - let ctx = 0u8; - - // Round 1 - let mut bcasts: BTreeMap = BTreeMap::new(); - let mut all_shares: BTreeMap> = BTreeMap::new(); - let mut secrets: BTreeMap = BTreeMap::new(); - - for id in 1..=u32::from(max_signers) { - let (bcast, shares, secret) = - kryptology::round1(id, threshold, max_signers, ctx, &mut rng).unwrap(); - bcasts.insert(id, bcast); - secrets.insert(id, secret); - for (&target_id, share) in &shares { - all_shares - .entry(target_id) - .or_default() - .insert(id, share.clone()); - } - } - - // Round 2 - let mut key_packages = BTreeMap::new(); - let mut public_key_packages = Vec::new(); - - for id in 1..=u32::from(max_signers) { - let received_bcasts: BTreeMap<_, _> = bcasts - .iter() - .filter(|(k, _)| **k != id) - .map(|(k, v)| (*k, v.clone())) - .collect(); - let received_shares = all_shares.remove(&id).unwrap(); - let secret = secrets.remove(&id).unwrap(); - - let (_r2_bcast, key_package, pub_package) = - kryptology::round2(secret, &received_bcasts, &received_shares).unwrap(); - key_packages.insert(id, key_package); - public_key_packages.push(pub_package); - } - - // BLS sign with only participants 1 and 2 (threshold = 2) - let message = b"threshold signing"; - let signers: [u32; 2] = [1, 2]; - - let partial_sigs: Vec<_> = signers - .iter() - .map(|&id| { - kryptology::BlsPartialSignature::from_key_package(id, &key_packages[&id], message) - }) - .collect(); - - let signature = kryptology::BlsSignature::from_partial_signatures(threshold, &partial_sigs) - .expect("BLS signature combination should succeed"); - - let vk = public_key_packages[0].verifying_key(); - assert!( - signature.verify(vk, message), - "BLS threshold signature should verify" - ); - - // Verify wrong message fails - assert!( - !signature.verify(vk, b"wrong message"), - "BLS signature should not verify against a different message" - ); -} - -/// Verify that an invalid proof is caught in round2. -#[test] -fn kryptology_invalid_proof_rejected() { - let mut rng = StdRng::seed_from_u64(99); - let threshold = 2u16; - let max_signers = 3u16; - let ctx = 0u8; - - let (mut bcast1, shares1, _secret1) = - kryptology::round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); - let (_bcast2, _shares2, secret2) = - kryptology::round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); - let (bcast3, shares3, _secret3) = - kryptology::round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); - - // Corrupt participant 1's proof (flip LSB of ci, keeping it a valid scalar) - bcast1.ci[31] ^= 0x01; - - // Participant 2 should reject participant 1's proof - let received_bcasts: BTreeMap = - [(1, bcast1.clone()), (3, bcast3.clone())].into(); - let received_shares: BTreeMap = - [(1, shares1[&2].clone()), (3, shares3[&2].clone())].into(); - - let result = kryptology::round2(secret2, &received_bcasts, &received_shares); - assert!(result.is_err()); - match result.unwrap_err() { - kryptology::DkgError::InvalidProof { culprit } => assert_eq!(culprit, 1), - other => panic!("expected InvalidProof, got {other:?}"), - } -} - -/// Verify that a share addressed to the wrong participant is rejected in -/// round2. -#[test] -fn kryptology_share_id_mismatch_rejected() { - let mut rng = StdRng::seed_from_u64(42); - let threshold = 2u16; - let max_signers = 3u16; - let ctx = 0u8; - - let (bcast1, shares1, _secret1) = - kryptology::round1(1, threshold, max_signers, ctx, &mut rng).unwrap(); - let (_bcast2, _shares2, secret2) = - kryptology::round1(2, threshold, max_signers, ctx, &mut rng).unwrap(); - let (bcast3, shares3, _secret3) = - kryptology::round1(3, threshold, max_signers, ctx, &mut rng).unwrap(); - - let received_bcasts: BTreeMap = [(1, bcast1), (3, bcast3)].into(); - - let mut wrong_share = shares1[&2].clone(); - wrong_share.id = 3; - let received_shares: BTreeMap = - [(1, wrong_share), (3, shares3[&2].clone())].into(); - - let result = kryptology::round2(secret2, &received_bcasts, &received_shares); - assert!(result.is_err()); - match result.unwrap_err() { - kryptology::DkgError::InvalidShare { culprit } => assert_eq!(culprit, 1), - other => panic!("expected InvalidShare, got {other:?}"), - } -} From bdf6bb778a097d3c1085f4cd9aaad9f2eaa8fc2e Mon Sep 17 00:00:00 2001 From: Quang Le Date: Sat, 2 May 2026 22:55:34 +0700 Subject: [PATCH 23/23] fix: ignore RUSTSEC-2026-0118 and RUSTSEC-2026-0119 because libp2p --- Cargo.lock | 2 +- deny.toml | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 39761778..8f444755 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5750,7 +5750,7 @@ version = "1.7.1" dependencies = [ "blst", "hex", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "serde", "serde_json", diff --git a/deny.toml b/deny.toml index 93786586..7a6d34c9 100644 --- a/deny.toml +++ b/deny.toml @@ -30,6 +30,13 @@ ignore = [ # (stuck on 0.7.3) and `alloy-signer-local` (stuck on 0.8.5). Neither is # reachable from Pluto's loggers. Remove once upstream bumps to >=0.9.3. { id = "RUSTSEC-2026-0097", reason = "transitive rand <0.9.3 via cuckoofilter and alloy-signer-local; not triggerable from our code" }, + # `hickory-proto` 0.26.1 exists, but current latest `libp2p` (0.56.0) + # still pins `libp2p-dns` / `libp2p-mdns` to Hickory 0.25.2. Pluto does not + # enable Hickory DNSSEC validation. Remove once libp2p moves to fixed Hickory. + { id = "RUSTSEC-2026-0118", reason = "transitive hickory-proto 0.25.2 via latest libp2p DNS/mDNS; DNSSEC validation path not enabled by Pluto" }, + # Same pinned `libp2p` DNS/mDNS stack as above. Cargo cannot select + # `hickory-proto` 0.26.1 until the libp2p crates relax their 0.25.2 pins. + { id = "RUSTSEC-2026-0119", reason = "transitive hickory-proto 0.25.2 via latest libp2p DNS/mDNS; fixed hickory exists but libp2p still pins 0.25.2" }, ] unmaintained = "workspace"