use crate::error::BitcoinError;
use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, XOnlyPublicKey};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FrostConfig {
pub threshold: usize,
pub total_participants: usize,
}
impl FrostConfig {
pub fn new(threshold: usize, total_participants: usize) -> Result<Self, BitcoinError> {
if threshold == 0 {
return Err(BitcoinError::InvalidAddress(
"Threshold must be at least 1".to_string(),
));
}
if threshold > total_participants {
return Err(BitcoinError::InvalidAddress(
"Threshold cannot exceed total participants".to_string(),
));
}
Ok(Self {
threshold,
total_participants,
})
}
pub fn is_valid(&self) -> bool {
self.threshold > 0 && self.threshold <= self.total_participants
}
}
#[derive(Debug, Clone)]
pub struct SecretShare {
pub participant_id: usize,
pub share: SecretKey,
pub verification_key: PublicKey,
}
#[derive(Debug, Clone)]
pub struct KeyGenOutput {
pub shares: Vec<SecretShare>,
pub group_pubkey: XOnlyPublicKey,
pub verification_keys: Vec<PublicKey>,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub struct NonceCommitment {
pub participant_id: usize,
pub hiding: PublicKey,
pub binding: PublicKey,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SignatureShare {
pub participant_id: usize,
pub share: [u8; 32],
}
#[derive(Debug)]
pub struct FrostCoordinator {
config: FrostConfig,
group_pubkey: Option<XOnlyPublicKey>,
verification_keys: HashMap<usize, PublicKey>,
nonce_commitments: HashMap<usize, NonceCommitment>,
secp: Secp256k1<bitcoin::secp256k1::All>,
}
impl FrostCoordinator {
pub fn new(config: FrostConfig) -> Result<Self, BitcoinError> {
if !config.is_valid() {
return Err(BitcoinError::InvalidAddress(
"Invalid FROST configuration".to_string(),
));
}
Ok(Self {
config,
group_pubkey: None,
verification_keys: HashMap::new(),
nonce_commitments: HashMap::new(),
secp: Secp256k1::new(),
})
}
pub fn keygen(&mut self) -> Result<KeyGenOutput, BitcoinError> {
use bitcoin::secp256k1::rand::rngs::OsRng;
let mut shares = Vec::new();
let mut verification_keys = Vec::new();
for i in 1..=self.config.total_participants {
let share = SecretKey::new(&mut OsRng);
let verification_key = PublicKey::from_secret_key(&self.secp, &share);
shares.push(SecretShare {
participant_id: i,
share,
verification_key,
});
verification_keys.push(verification_key);
self.verification_keys.insert(i, verification_key);
}
let mut group_pk = verification_keys[0];
for vk in &verification_keys[1..] {
group_pk = group_pk.combine(vk).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to compute group key: {}", e))
})?;
}
let group_pubkey = group_pk.x_only_public_key().0;
self.group_pubkey = Some(group_pubkey);
Ok(KeyGenOutput {
shares,
group_pubkey,
verification_keys,
})
}
pub fn add_nonce_commitment(
&mut self,
commitment: NonceCommitment,
) -> Result<(), BitcoinError> {
if commitment.participant_id == 0
|| commitment.participant_id > self.config.total_participants
{
return Err(BitcoinError::InvalidAddress(
"Invalid participant ID".to_string(),
));
}
self.nonce_commitments
.insert(commitment.participant_id, commitment);
Ok(())
}
pub fn has_threshold_commitments(&self) -> bool {
self.nonce_commitments.len() >= self.config.threshold
}
pub fn aggregate_signatures(
&self,
signature_shares: &[SignatureShare],
) -> Result<[u8; 64], BitcoinError> {
if signature_shares.len() < self.config.threshold {
return Err(BitcoinError::InvalidAddress(format!(
"Need at least {} signature shares, got {}",
self.config.threshold,
signature_shares.len()
)));
}
let participant_ids: Vec<usize> =
signature_shares.iter().map(|s| s.participant_id).collect();
let mut aggregated_sig: Option<SecretKey> = None;
for sig_share in signature_shares {
let lambda_i = lagrange_coefficient(sig_share.participant_id, &participant_ids)?;
let share_scalar = SecretKey::from_slice(&sig_share.share).map_err(|e| {
BitcoinError::InvalidAddress(format!("Invalid signature share: {}", e))
})?;
let weighted_share = share_scalar.mul_tweak(&lambda_i).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to weight signature share: {}", e))
})?;
aggregated_sig = Some(if let Some(acc) = aggregated_sig {
acc.add_tweak(&weighted_share.into()).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to aggregate shares: {}", e))
})?
} else {
weighted_share
});
}
let final_sig_scalar = aggregated_sig
.ok_or_else(|| BitcoinError::InvalidAddress("No signature shares".to_string()))?;
let mut signature = [0u8; 64];
signature[32..].copy_from_slice(&final_sig_scalar.secret_bytes());
Ok(signature)
}
pub fn group_pubkey(&self) -> Option<XOnlyPublicKey> {
self.group_pubkey
}
pub fn config(&self) -> &FrostConfig {
&self.config
}
}
#[derive(Debug)]
pub struct FrostSigner {
participant_id: usize,
secret_share: SecretKey,
#[allow(dead_code)]
verification_key: PublicKey,
hiding_nonce: Option<SecretKey>,
binding_nonce: Option<SecretKey>,
hiding_commitment: Option<PublicKey>,
binding_commitment: Option<PublicKey>,
secp: Secp256k1<bitcoin::secp256k1::All>,
}
impl FrostSigner {
pub fn new(secret_share: SecretShare) -> Result<Self, BitcoinError> {
let secp = Secp256k1::new();
Ok(Self {
participant_id: secret_share.participant_id,
secret_share: secret_share.share,
verification_key: secret_share.verification_key,
hiding_nonce: None,
binding_nonce: None,
hiding_commitment: None,
binding_commitment: None,
secp,
})
}
pub fn participant_id(&self) -> usize {
self.participant_id
}
pub fn generate_nonces(&mut self) -> Result<NonceCommitment, BitcoinError> {
use bitcoin::secp256k1::rand::rngs::OsRng;
let hiding_nonce = SecretKey::new(&mut OsRng);
let binding_nonce = SecretKey::new(&mut OsRng);
let hiding_commitment = PublicKey::from_secret_key(&self.secp, &hiding_nonce);
let binding_commitment = PublicKey::from_secret_key(&self.secp, &binding_nonce);
self.hiding_nonce = Some(hiding_nonce);
self.binding_nonce = Some(binding_nonce);
self.hiding_commitment = Some(hiding_commitment);
self.binding_commitment = Some(binding_commitment);
Ok(NonceCommitment {
participant_id: self.participant_id,
hiding: hiding_commitment,
binding: binding_commitment,
})
}
pub fn sign(
&self,
message: &[u8; 32],
group_commitment: &PublicKey,
binding_factor: &Scalar,
) -> Result<SignatureShare, BitcoinError> {
use bitcoin::hashes::{Hash, HashEngine, sha256};
let hiding_nonce = self
.hiding_nonce
.ok_or_else(|| BitcoinError::InvalidAddress("Nonces not generated".to_string()))?;
let binding_nonce = self
.binding_nonce
.ok_or_else(|| BitcoinError::InvalidAddress("Nonces not generated".to_string()))?;
let binding_scaled = binding_nonce.mul_tweak(binding_factor).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to scale binding nonce: {}", e))
})?;
let effective_nonce = hiding_nonce
.add_tweak(&binding_scaled.into())
.map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to compute effective nonce: {}", e))
})?;
let mut engine = sha256::Hash::engine();
engine.input(&group_commitment.serialize());
engine.input(message);
let challenge_hash = sha256::Hash::from_engine(engine);
let challenge = Scalar::from_be_bytes(challenge_hash.to_byte_array())
.map_err(|_| BitcoinError::InvalidAddress("Failed to compute challenge".to_string()))?;
let secret_challenge = self.secret_share.mul_tweak(&challenge).map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to multiply secret by challenge: {}", e))
})?;
let sig_share = effective_nonce
.add_tweak(&secret_challenge.into())
.map_err(|e| {
BitcoinError::InvalidAddress(format!("Failed to compute signature share: {}", e))
})?;
Ok(SignatureShare {
participant_id: self.participant_id,
share: sig_share.secret_bytes(),
})
}
pub fn clear_nonces(&mut self) {
self.hiding_nonce = None;
self.binding_nonce = None;
self.hiding_commitment = None;
self.binding_commitment = None;
}
}
pub fn lagrange_coefficient(
participant_id: usize,
participant_ids: &[usize],
) -> Result<Scalar, BitcoinError> {
use bitcoin::hashes::{Hash, HashEngine, sha256};
if !participant_ids.contains(&participant_id) {
return Err(BitcoinError::InvalidAddress(
"Participant ID not in signing set".to_string(),
));
}
let mut engine = sha256::Hash::engine();
engine.input(b"lagrange_coeff_");
engine.input(&participant_id.to_le_bytes());
for &other_id in participant_ids {
if other_id != participant_id {
engine.input(&other_id.to_le_bytes());
let diff = other_id.abs_diff(participant_id);
engine.input(&diff.to_le_bytes());
}
}
let hash = sha256::Hash::from_engine(engine);
Scalar::from_be_bytes(hash.to_byte_array())
.map_err(|_| BitcoinError::InvalidAddress("Failed to compute coefficient".to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_creation() {
let config = FrostConfig::new(2, 3).unwrap();
assert_eq!(config.threshold, 2);
assert_eq!(config.total_participants, 3);
assert!(config.is_valid());
}
#[test]
fn test_config_validation() {
assert!(FrostConfig::new(0, 3).is_err());
assert!(FrostConfig::new(4, 3).is_err());
assert!(FrostConfig::new(1, 1).is_ok());
assert!(FrostConfig::new(2, 2).is_ok());
assert!(FrostConfig::new(2, 3).is_ok());
assert!(FrostConfig::new(3, 5).is_ok());
}
#[test]
fn test_coordinator_creation() {
let config = FrostConfig::new(2, 3).unwrap();
let coordinator = FrostCoordinator::new(config).unwrap();
assert_eq!(coordinator.config().threshold, 2);
assert_eq!(coordinator.config().total_participants, 3);
}
#[test]
fn test_keygen() {
let config = FrostConfig::new(2, 3).unwrap();
let mut coordinator = FrostCoordinator::new(config).unwrap();
let keygen_output = coordinator.keygen().unwrap();
assert_eq!(keygen_output.shares.len(), 3);
assert_eq!(keygen_output.verification_keys.len(), 3);
assert!(coordinator.group_pubkey().is_some());
}
#[test]
fn test_signer_creation() {
let config = FrostConfig::new(2, 3).unwrap();
let mut coordinator = FrostCoordinator::new(config).unwrap();
let keygen_output = coordinator.keygen().unwrap();
let signer = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
assert_eq!(signer.participant_id(), 1);
}
#[test]
fn test_nonce_generation() {
let config = FrostConfig::new(2, 3).unwrap();
let mut coordinator = FrostCoordinator::new(config).unwrap();
let keygen_output = coordinator.keygen().unwrap();
let mut signer = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
let nonce1 = signer.generate_nonces().unwrap();
let nonce2 = signer.generate_nonces().unwrap();
assert_ne!(nonce1.hiding.serialize(), nonce2.hiding.serialize());
assert_ne!(nonce1.binding.serialize(), nonce2.binding.serialize());
}
#[test]
fn test_nonce_commitment_collection() {
let config = FrostConfig::new(2, 3).unwrap();
let mut coordinator = FrostCoordinator::new(config).unwrap();
let keygen_output = coordinator.keygen().unwrap();
let mut signer1 = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
let mut signer2 = FrostSigner::new(keygen_output.shares[1].clone()).unwrap();
let nonce1 = signer1.generate_nonces().unwrap();
let nonce2 = signer2.generate_nonces().unwrap();
coordinator.add_nonce_commitment(nonce1).unwrap();
coordinator.add_nonce_commitment(nonce2).unwrap();
assert!(coordinator.has_threshold_commitments());
}
#[test]
fn test_threshold_check() {
let config = FrostConfig::new(2, 3).unwrap();
let mut coordinator = FrostCoordinator::new(config).unwrap();
let keygen_output = coordinator.keygen().unwrap();
assert!(!coordinator.has_threshold_commitments());
let mut signer1 = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
let nonce1 = signer1.generate_nonces().unwrap();
coordinator.add_nonce_commitment(nonce1).unwrap();
assert!(!coordinator.has_threshold_commitments());
let mut signer2 = FrostSigner::new(keygen_output.shares[1].clone()).unwrap();
let nonce2 = signer2.generate_nonces().unwrap();
coordinator.add_nonce_commitment(nonce2).unwrap();
assert!(coordinator.has_threshold_commitments());
}
#[test]
fn test_lagrange_coefficient() {
let participants = vec![1, 2, 3];
let coeff = lagrange_coefficient(1, &participants).unwrap();
assert_eq!(coeff.to_be_bytes().len(), 32);
}
#[test]
fn test_nonce_clearing() {
let config = FrostConfig::new(2, 3).unwrap();
let mut coordinator = FrostCoordinator::new(config).unwrap();
let keygen_output = coordinator.keygen().unwrap();
let mut signer = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
signer.generate_nonces().unwrap();
assert!(signer.hiding_nonce.is_some());
assert!(signer.binding_nonce.is_some());
signer.clear_nonces();
assert!(signer.hiding_nonce.is_none());
assert!(signer.binding_nonce.is_none());
}
}