use crate::dkg::{DkgParams, DkgParticipant};
use crate::signing::{PublicKey, SignatureBytes};
use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
use curve25519_dalek::ristretto::RistrettoPoint;
use curve25519_dalek::scalar::Scalar;
use serde::{Deserialize, Serialize};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum FrostError {
#[error("Invalid threshold: must be 1 <= t <= n")]
InvalidThreshold,
#[error("Insufficient signers: need at least {0} but got {1}")]
InsufficientSigners(usize, usize),
#[error("Invalid signer ID: {0}")]
InvalidSignerId(usize),
#[error("Duplicate signer ID: {0}")]
DuplicateSignerId(usize),
#[error("Missing nonce commitment for signer {0}")]
MissingFrostNonceCommitment(usize),
#[error("Nonce not preprocessed")]
NonceNotPreprocessed,
#[error("Invalid signature share")]
InvalidSignatureShare,
#[error("Serialization error: {0}")]
SerializationError(String),
}
pub type FrostResult<T> = Result<T, FrostError>;
#[derive(Clone, Serialize, Deserialize)]
pub struct FrostSecretShare {
pub index: usize,
pub secret: Scalar,
pub verification_shares: Vec<RistrettoPoint>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct FrostNonceCommitment {
pub hiding: RistrettoPoint,
pub binding: RistrettoPoint,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PartialSignature {
pub signer_id: usize,
pub z: Scalar,
}
pub struct FrostKeygen {
threshold: usize,
num_participants: usize,
group_public_key: Option<PublicKey>,
shares: Vec<FrostSecretShare>,
}
impl FrostKeygen {
pub fn new(threshold: usize, num_participants: usize) -> Self {
Self {
threshold,
num_participants,
group_public_key: None,
shares: Vec::new(),
}
}
pub fn generate_shares(&mut self) -> Vec<FrostSecretShare> {
let params = DkgParams::new(self.num_participants, self.threshold);
let mut participants: Vec<_> = (0..self.num_participants)
.map(|i| DkgParticipant::new(¶ms, i))
.collect();
let commitments: Vec<_> = participants.iter().map(|p| p.get_commitments()).collect();
for i in 0..self.num_participants {
for j in 0..self.num_participants {
if i != j {
let share = participants[j].generate_share(i).unwrap();
participants[i]
.receive_share(j, share, &commitments[j])
.unwrap();
}
}
}
self.shares = participants
.iter()
.enumerate()
.map(|(i, p)| {
let secret = p.get_secret_share().unwrap();
FrostSecretShare {
index: i + 1,
secret,
verification_shares: commitments
.iter()
.map(|c| {
use curve25519_dalek::ristretto::CompressedRistretto;
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&c.commitments[0]);
CompressedRistretto(bytes).decompress().unwrap()
})
.collect(),
}
})
.collect();
let group_point = crate::dkg::aggregate_public_key(&commitments);
self.group_public_key = Some(group_point.compress().to_bytes());
self.shares.clone()
}
pub fn group_public_key(&self) -> PublicKey {
self.group_public_key.unwrap()
}
}
pub struct FrostSigner {
signer_id: usize,
secret_share: FrostSecretShare,
group_public_key: PublicKey,
nonce_pair: Option<(Scalar, Scalar)>,
nonce_commitment: Option<FrostNonceCommitment>,
}
impl FrostSigner {
pub fn new(
signer_id: usize,
secret_share: FrostSecretShare,
group_public_key: PublicKey,
) -> Self {
Self {
signer_id,
secret_share,
group_public_key,
nonce_pair: None,
nonce_commitment: None,
}
}
pub fn preprocess(&mut self) {
let d = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>());
let e = Scalar::from_bytes_mod_order(rand::random::<[u8; 32]>());
let hiding = d * RISTRETTO_BASEPOINT_POINT;
let binding = e * RISTRETTO_BASEPOINT_POINT;
self.nonce_pair = Some((d, e));
self.nonce_commitment = Some(FrostNonceCommitment { hiding, binding });
}
pub fn get_nonce_commitment(&self) -> FrostNonceCommitment {
self.nonce_commitment
.clone()
.expect("Nonce not preprocessed")
}
pub fn sign(
&self,
message: &[u8],
signing_set: &[usize],
commitments: &[FrostNonceCommitment],
) -> FrostResult<PartialSignature> {
if self.nonce_pair.is_none() {
return Err(FrostError::NonceNotPreprocessed);
}
let (d, e) = self.nonce_pair.unwrap();
let rho = compute_binding_value(message, commitments);
let group_commitment: RistrettoPoint =
commitments.iter().map(|c| c.hiding + rho * c.binding).sum();
let challenge = compute_challenge(&self.group_public_key, &group_commitment, message);
let lambda = compute_lagrange_coefficient(self.signer_id, signing_set);
let z = d + (rho * e) + (lambda * self.secret_share.secret * challenge);
Ok(PartialSignature {
signer_id: self.signer_id,
z,
})
}
}
pub fn aggregate_frost_signatures(
message: &[u8],
signing_set: &[usize],
commitments: &[FrostNonceCommitment],
partial_sigs: &[PartialSignature],
) -> FrostResult<SignatureBytes> {
if partial_sigs.len() < signing_set.len() {
return Err(FrostError::InsufficientSigners(
signing_set.len(),
partial_sigs.len(),
));
}
let rho = compute_binding_value(message, commitments);
let group_commitment: RistrettoPoint =
commitments.iter().map(|c| c.hiding + rho * c.binding).sum();
let z: Scalar = partial_sigs.iter().map(|sig| sig.z).sum();
let r_bytes = group_commitment.compress().to_bytes();
let z_bytes = z.to_bytes();
let mut sig_bytes = [0u8; 64];
sig_bytes[..32].copy_from_slice(&r_bytes);
sig_bytes[32..].copy_from_slice(&z_bytes);
Ok(sig_bytes)
}
fn compute_binding_value(message: &[u8], commitments: &[FrostNonceCommitment]) -> Scalar {
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(message);
for commitment in commitments {
hasher.update(&commitment.hiding.compress().to_bytes());
hasher.update(&commitment.binding.compress().to_bytes());
}
let hash = hasher.finalize();
Scalar::from_bytes_mod_order(*hash.as_bytes())
}
fn compute_challenge(group_pk: &PublicKey, r: &RistrettoPoint, message: &[u8]) -> Scalar {
use blake3::Hasher;
let mut hasher = Hasher::new();
hasher.update(group_pk);
hasher.update(&r.compress().to_bytes());
hasher.update(message);
let hash = hasher.finalize();
Scalar::from_bytes_mod_order(*hash.as_bytes())
}
fn compute_lagrange_coefficient(signer_id: usize, signing_set: &[usize]) -> Scalar {
let mut numerator = Scalar::ONE;
let mut denominator = Scalar::ONE;
for &j in signing_set {
if j != signer_id {
numerator *= Scalar::from(j as u64);
denominator *= Scalar::from(j as u64) - Scalar::from(signer_id as u64);
}
}
numerator * denominator.invert()
}
pub fn verify_frost_signature(
public_key: &PublicKey,
message: &[u8],
signature: &SignatureBytes,
) -> FrostResult<()> {
use curve25519_dalek::ristretto::CompressedRistretto;
let mut r_bytes = [0u8; 32];
let mut z_bytes = [0u8; 32];
r_bytes.copy_from_slice(&signature[..32]);
z_bytes.copy_from_slice(&signature[32..]);
let r_point = CompressedRistretto(r_bytes)
.decompress()
.ok_or(FrostError::InvalidSignatureShare)?;
let z = Scalar::from_bytes_mod_order(z_bytes);
let pk_point = CompressedRistretto(*public_key)
.decompress()
.ok_or(FrostError::InvalidSignatureShare)?;
let challenge = compute_challenge(public_key, &r_point, message);
let lhs = z * RISTRETTO_BASEPOINT_POINT;
let rhs = r_point + (challenge * pk_point);
if lhs == rhs {
Ok(())
} else {
Err(FrostError::InvalidSignatureShare)
}
}
#[cfg(test)]
mod tests {
use super::*;
use curve25519_dalek::traits::Identity;
#[test]
fn test_frost_2_of_3_basic() {
let threshold = 2;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let mut signers: Vec<_> = shares
.iter()
.enumerate()
.map(|(i, share)| FrostSigner::new(i + 1, share.clone(), group_pk))
.collect();
for signer in &mut signers {
signer.preprocess();
}
let message = b"FROST test message";
let signing_set = vec![1, 2];
let commitments: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].get_nonce_commitment())
.collect();
let partial_sigs: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].sign(message, &signing_set, &commitments))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let signature =
aggregate_frost_signatures(message, &signing_set, &commitments, &partial_sigs).unwrap();
assert!(verify_frost_signature(&group_pk, message, &signature).is_ok());
}
#[test]
fn test_frost_different_signing_sets() {
let threshold = 2;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let message = b"Test message";
let signing_sets = vec![vec![1, 2], vec![1, 3], vec![2, 3]];
for signing_set in signing_sets {
let mut signers: Vec<_> = shares
.iter()
.enumerate()
.map(|(i, share)| FrostSigner::new(i + 1, share.clone(), group_pk))
.collect();
for signer in &mut signers {
signer.preprocess();
}
let commitments: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].get_nonce_commitment())
.collect();
let partial_sigs: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].sign(message, &signing_set, &commitments))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let signature =
aggregate_frost_signatures(message, &signing_set, &commitments, &partial_sigs)
.unwrap();
assert!(verify_frost_signature(&group_pk, message, &signature).is_ok());
}
}
#[test]
fn test_frost_3_of_5() {
let threshold = 3;
let num_signers = 5;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let mut signers: Vec<_> = shares
.iter()
.enumerate()
.map(|(i, share)| FrostSigner::new(i + 1, share.clone(), group_pk))
.collect();
for signer in &mut signers {
signer.preprocess();
}
let message = b"3-of-5 threshold test";
let signing_set = vec![1, 3, 5];
let commitments: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].get_nonce_commitment())
.collect();
let partial_sigs: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].sign(message, &signing_set, &commitments))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let signature =
aggregate_frost_signatures(message, &signing_set, &commitments, &partial_sigs).unwrap();
assert!(verify_frost_signature(&group_pk, message, &signature).is_ok());
}
#[test]
fn test_frost_wrong_message_fails() {
let threshold = 2;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let mut signers: Vec<_> = shares
.iter()
.enumerate()
.map(|(i, share)| FrostSigner::new(i + 1, share.clone(), group_pk))
.collect();
for signer in &mut signers {
signer.preprocess();
}
let message = b"Original message";
let wrong_message = b"Wrong message";
let signing_set = vec![1, 2];
let commitments: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].get_nonce_commitment())
.collect();
let partial_sigs: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].sign(message, &signing_set, &commitments))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let signature =
aggregate_frost_signatures(message, &signing_set, &commitments, &partial_sigs).unwrap();
assert!(verify_frost_signature(&group_pk, wrong_message, &signature).is_err());
}
#[test]
fn test_frost_multiple_signatures_same_key() {
let threshold = 2;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let messages = vec![b"Message 1".as_slice(), b"Message 2", b"Message 3"];
for message in messages {
let mut signers: Vec<_> = shares
.iter()
.enumerate()
.map(|(i, share)| FrostSigner::new(i + 1, share.clone(), group_pk))
.collect();
for signer in &mut signers {
signer.preprocess();
}
let signing_set = vec![1, 2];
let commitments: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].get_nonce_commitment())
.collect();
let partial_sigs: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].sign(message, &signing_set, &commitments))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let signature =
aggregate_frost_signatures(message, &signing_set, &commitments, &partial_sigs)
.unwrap();
assert!(verify_frost_signature(&group_pk, message, &signature).is_ok());
}
}
#[test]
fn test_frost_lagrange_coefficient() {
let signing_set = vec![1, 2, 3];
let lambda1 = compute_lagrange_coefficient(1, &signing_set);
let lambda2 = compute_lagrange_coefficient(2, &signing_set);
let lambda3 = compute_lagrange_coefficient(3, &signing_set);
assert_ne!(lambda1, Scalar::ZERO);
assert_ne!(lambda2, Scalar::ZERO);
assert_ne!(lambda3, Scalar::ZERO);
}
#[test]
fn test_frost_nonce_not_preprocessed() {
let threshold = 2;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let signer = FrostSigner::new(1, shares[0].clone(), group_pk);
let message = b"Test";
let signing_set = vec![1, 2];
let commitments = vec![];
let result = signer.sign(message, &signing_set, &commitments);
assert!(matches!(result, Err(FrostError::NonceNotPreprocessed)));
}
#[test]
fn test_frost_serialization() {
let threshold = 2;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let share_bytes = crate::codec::encode(&shares[0]).unwrap();
let deserialized_share: FrostSecretShare = crate::codec::decode(&share_bytes).unwrap();
assert_eq!(shares[0].index, deserialized_share.index);
let mut signer = FrostSigner::new(1, shares[0].clone(), keygen.group_public_key());
signer.preprocess();
let commitment = signer.get_nonce_commitment();
let commitment_bytes = crate::codec::encode(&commitment).unwrap();
let deserialized_commitment: FrostNonceCommitment =
crate::codec::decode(&commitment_bytes).unwrap();
assert_eq!(
commitment.hiding.compress().to_bytes(),
deserialized_commitment.hiding.compress().to_bytes()
);
}
#[test]
fn test_frost_all_participants() {
let threshold = 3;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let mut signers: Vec<_> = shares
.iter()
.enumerate()
.map(|(i, share)| FrostSigner::new(i + 1, share.clone(), group_pk))
.collect();
for signer in &mut signers {
signer.preprocess();
}
let message = b"All participants signing";
let signing_set = vec![1, 2, 3];
let commitments: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].get_nonce_commitment())
.collect();
let partial_sigs: Vec<_> = signing_set
.iter()
.map(|&id| signers[id - 1].sign(message, &signing_set, &commitments))
.collect::<Result<Vec<_>, _>>()
.unwrap();
let signature =
aggregate_frost_signatures(message, &signing_set, &commitments, &partial_sigs).unwrap();
assert!(verify_frost_signature(&group_pk, message, &signature).is_ok());
}
#[test]
fn test_frost_deterministic_keygen() {
let threshold = 2;
let num_signers = 3;
let mut keygen1 = FrostKeygen::new(threshold, num_signers);
let shares1 = keygen1.generate_shares();
let pk1 = keygen1.group_public_key();
let mut keygen2 = FrostKeygen::new(threshold, num_signers);
let shares2 = keygen2.generate_shares();
let pk2 = keygen2.group_public_key();
assert_ne!(pk1, pk2);
assert_ne!(shares1[0].secret, shares2[0].secret);
}
#[test]
fn test_lagrange_interpolation_property() {
let threshold = 2;
let num_signers = 3;
let mut keygen = FrostKeygen::new(threshold, num_signers);
let shares = keygen.generate_shares();
let group_pk = keygen.group_public_key();
let signing_sets = vec![vec![1, 2], vec![1, 3], vec![2, 3]];
for signing_set in signing_sets {
let mut interpolated_pk = RistrettoPoint::identity();
for &signer_id in &signing_set {
let lambda = compute_lagrange_coefficient(signer_id, &signing_set);
let share_secret = shares[signer_id - 1].secret;
interpolated_pk += lambda * share_secret * RISTRETTO_BASEPOINT_POINT;
}
let interpolated_bytes = interpolated_pk.compress().to_bytes();
use curve25519_dalek::ristretto::CompressedRistretto;
let group_pk_point = CompressedRistretto(group_pk).decompress().unwrap();
let group_pk_bytes = group_pk_point.compress().to_bytes();
assert_eq!(
interpolated_bytes, group_pk_bytes,
"Lagrange interpolation failed for signing set {:?}",
signing_set
);
}
}
}