kaccy_bitcoin/
frost.rs

1//! FROST: Flexible Round-Optimized Schnorr Threshold Signatures
2//!
3//! Implements threshold signatures allowing T-of-N parties to sign,
4//! producing a signature indistinguishable from a single-key signature.
5//!
6//! # Overview
7//!
8//! FROST provides:
9//! - Threshold signing (T-of-N instead of N-of-N)
10//! - Privacy (looks like single-sig on-chain)
11//! - Flexibility (dynamic signer selection)
12//! - Efficiency (2-round signing protocol)
13//!
14//! # Example
15//!
16//! ```
17//! use kaccy_bitcoin::frost::{FrostCoordinator, FrostSigner, FrostConfig};
18//!
19//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
20//! // Create a 2-of-3 threshold scheme
21//! let config = FrostConfig::new(2, 3)?;
22//! let coordinator = FrostCoordinator::new(config)?;
23//!
24//! // Generate shares for each participant
25//! # Ok(())
26//! # }
27//! ```
28
29use crate::error::BitcoinError;
30use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, XOnlyPublicKey};
31use serde::{Deserialize, Serialize};
32use std::collections::HashMap;
33
34/// FROST configuration for T-of-N threshold
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct FrostConfig {
37    /// Threshold (minimum signers required)
38    pub threshold: usize,
39    /// Total number of participants
40    pub total_participants: usize,
41}
42
43impl FrostConfig {
44    /// Create a new FROST configuration
45    pub fn new(threshold: usize, total_participants: usize) -> Result<Self, BitcoinError> {
46        if threshold == 0 {
47            return Err(BitcoinError::InvalidAddress(
48                "Threshold must be at least 1".to_string(),
49            ));
50        }
51
52        if threshold > total_participants {
53            return Err(BitcoinError::InvalidAddress(
54                "Threshold cannot exceed total participants".to_string(),
55            ));
56        }
57
58        Ok(Self {
59            threshold,
60            total_participants,
61        })
62    }
63
64    /// Check if this is a valid T-of-N configuration
65    pub fn is_valid(&self) -> bool {
66        self.threshold > 0 && self.threshold <= self.total_participants
67    }
68}
69
70/// Secret share for a participant
71#[derive(Debug, Clone)]
72pub struct SecretShare {
73    /// Participant ID (1-indexed)
74    pub participant_id: usize,
75    /// Secret share value
76    pub share: SecretKey,
77    /// Verification share (public key corresponding to secret share)
78    pub verification_key: PublicKey,
79}
80
81/// FROST key generation output
82#[derive(Debug, Clone)]
83pub struct KeyGenOutput {
84    /// Secret shares for each participant
85    pub shares: Vec<SecretShare>,
86    /// Group public key
87    pub group_pubkey: XOnlyPublicKey,
88    /// Verification keys for all participants
89    pub verification_keys: Vec<PublicKey>,
90}
91
92/// FROST nonce commitment
93#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
94pub struct NonceCommitment {
95    /// Participant ID
96    pub participant_id: usize,
97    /// Hiding nonce commitment (D_i)
98    pub hiding: PublicKey,
99    /// Binding nonce commitment (E_i)
100    pub binding: PublicKey,
101}
102
103/// FROST signature share
104#[derive(Debug, Clone, Serialize, Deserialize)]
105pub struct SignatureShare {
106    /// Participant ID
107    pub participant_id: usize,
108    /// Signature share value
109    pub share: [u8; 32],
110}
111
112/// FROST coordinator for managing threshold signing
113#[derive(Debug)]
114pub struct FrostCoordinator {
115    /// Configuration
116    config: FrostConfig,
117    /// Group public key (if key generation completed)
118    group_pubkey: Option<XOnlyPublicKey>,
119    /// Verification keys for participants
120    verification_keys: HashMap<usize, PublicKey>,
121    /// Collected nonce commitments
122    nonce_commitments: HashMap<usize, NonceCommitment>,
123    /// Secp256k1 context
124    secp: Secp256k1<bitcoin::secp256k1::All>,
125}
126
127impl FrostCoordinator {
128    /// Create a new FROST coordinator
129    pub fn new(config: FrostConfig) -> Result<Self, BitcoinError> {
130        if !config.is_valid() {
131            return Err(BitcoinError::InvalidAddress(
132                "Invalid FROST configuration".to_string(),
133            ));
134        }
135
136        Ok(Self {
137            config,
138            group_pubkey: None,
139            verification_keys: HashMap::new(),
140            nonce_commitments: HashMap::new(),
141            secp: Secp256k1::new(),
142        })
143    }
144
145    /// Perform distributed key generation (simplified version)
146    pub fn keygen(&mut self) -> Result<KeyGenOutput, BitcoinError> {
147        use bitcoin::secp256k1::rand::rngs::OsRng;
148
149        let mut shares = Vec::new();
150        let mut verification_keys = Vec::new();
151
152        // Generate secret shares (simplified - real DKG is more complex)
153        for i in 1..=self.config.total_participants {
154            let share = SecretKey::new(&mut OsRng);
155            let verification_key = PublicKey::from_secret_key(&self.secp, &share);
156
157            shares.push(SecretShare {
158                participant_id: i,
159                share,
160                verification_key,
161            });
162
163            verification_keys.push(verification_key);
164            self.verification_keys.insert(i, verification_key);
165        }
166
167        // Compute group public key (simplified - sum of verification keys)
168        let mut group_pk = verification_keys[0];
169        for vk in &verification_keys[1..] {
170            group_pk = group_pk.combine(vk).map_err(|e| {
171                BitcoinError::InvalidAddress(format!("Failed to compute group key: {}", e))
172            })?;
173        }
174
175        let group_pubkey = group_pk.x_only_public_key().0;
176        self.group_pubkey = Some(group_pubkey);
177
178        Ok(KeyGenOutput {
179            shares,
180            group_pubkey,
181            verification_keys,
182        })
183    }
184
185    /// Add a nonce commitment from a participant
186    pub fn add_nonce_commitment(
187        &mut self,
188        commitment: NonceCommitment,
189    ) -> Result<(), BitcoinError> {
190        if commitment.participant_id == 0
191            || commitment.participant_id > self.config.total_participants
192        {
193            return Err(BitcoinError::InvalidAddress(
194                "Invalid participant ID".to_string(),
195            ));
196        }
197
198        self.nonce_commitments
199            .insert(commitment.participant_id, commitment);
200        Ok(())
201    }
202
203    /// Check if we have enough commitments to proceed
204    pub fn has_threshold_commitments(&self) -> bool {
205        self.nonce_commitments.len() >= self.config.threshold
206    }
207
208    /// Aggregate signature shares into final signature using Lagrange interpolation
209    pub fn aggregate_signatures(
210        &self,
211        signature_shares: &[SignatureShare],
212    ) -> Result<[u8; 64], BitcoinError> {
213        if signature_shares.len() < self.config.threshold {
214            return Err(BitcoinError::InvalidAddress(format!(
215                "Need at least {} signature shares, got {}",
216                self.config.threshold,
217                signature_shares.len()
218            )));
219        }
220
221        // Collect participant IDs for Lagrange interpolation
222        let participant_ids: Vec<usize> =
223            signature_shares.iter().map(|s| s.participant_id).collect();
224
225        // Aggregate signature shares using Lagrange interpolation
226        // Final signature: s = ∑(λ_i * s_i) where λ_i is Lagrange coefficient
227        let mut aggregated_sig: Option<SecretKey> = None;
228
229        for sig_share in signature_shares {
230            // Compute Lagrange coefficient for this participant
231            let lambda_i = lagrange_coefficient(sig_share.participant_id, &participant_ids)?;
232
233            // Load signature share as a scalar
234            let share_scalar = SecretKey::from_slice(&sig_share.share).map_err(|e| {
235                BitcoinError::InvalidAddress(format!("Invalid signature share: {}", e))
236            })?;
237
238            // Multiply signature share by Lagrange coefficient: λ_i * s_i
239            let weighted_share = share_scalar.mul_tweak(&lambda_i).map_err(|e| {
240                BitcoinError::InvalidAddress(format!("Failed to weight signature share: {}", e))
241            })?;
242
243            // Add to accumulator
244            aggregated_sig = Some(if let Some(acc) = aggregated_sig {
245                acc.add_tweak(&weighted_share.into()).map_err(|e| {
246                    BitcoinError::InvalidAddress(format!("Failed to aggregate shares: {}", e))
247                })?
248            } else {
249                weighted_share
250            });
251        }
252
253        let final_sig_scalar = aggregated_sig
254            .ok_or_else(|| BitcoinError::InvalidAddress("No signature shares".to_string()))?;
255
256        // Format as Schnorr signature (64 bytes: R || s)
257        let mut signature = [0u8; 64];
258        // In a full implementation, R would be the aggregated nonce commitment
259        // For now, use a placeholder R and put the aggregated s in the second half
260        signature[32..].copy_from_slice(&final_sig_scalar.secret_bytes());
261
262        Ok(signature)
263    }
264
265    /// Get the group public key
266    pub fn group_pubkey(&self) -> Option<XOnlyPublicKey> {
267        self.group_pubkey
268    }
269
270    /// Get configuration
271    pub fn config(&self) -> &FrostConfig {
272        &self.config
273    }
274}
275
276/// FROST signer for a single participant
277#[derive(Debug)]
278pub struct FrostSigner {
279    /// Participant ID
280    participant_id: usize,
281    /// Secret share
282    secret_share: SecretKey,
283    /// Verification key
284    #[allow(dead_code)]
285    verification_key: PublicKey,
286    /// Hiding nonce secret
287    hiding_nonce: Option<SecretKey>,
288    /// Binding nonce secret
289    binding_nonce: Option<SecretKey>,
290    /// Hiding nonce commitment
291    hiding_commitment: Option<PublicKey>,
292    /// Binding nonce commitment
293    binding_commitment: Option<PublicKey>,
294    /// Secp256k1 context
295    secp: Secp256k1<bitcoin::secp256k1::All>,
296}
297
298impl FrostSigner {
299    /// Create a new FROST signer with a secret share
300    pub fn new(secret_share: SecretShare) -> Result<Self, BitcoinError> {
301        let secp = Secp256k1::new();
302
303        Ok(Self {
304            participant_id: secret_share.participant_id,
305            secret_share: secret_share.share,
306            verification_key: secret_share.verification_key,
307            hiding_nonce: None,
308            binding_nonce: None,
309            hiding_commitment: None,
310            binding_commitment: None,
311            secp,
312        })
313    }
314
315    /// Get participant ID
316    pub fn participant_id(&self) -> usize {
317        self.participant_id
318    }
319
320    /// Generate nonce commitments for a signing round
321    pub fn generate_nonces(&mut self) -> Result<NonceCommitment, BitcoinError> {
322        use bitcoin::secp256k1::rand::rngs::OsRng;
323
324        let hiding_nonce = SecretKey::new(&mut OsRng);
325        let binding_nonce = SecretKey::new(&mut OsRng);
326
327        let hiding_commitment = PublicKey::from_secret_key(&self.secp, &hiding_nonce);
328        let binding_commitment = PublicKey::from_secret_key(&self.secp, &binding_nonce);
329
330        self.hiding_nonce = Some(hiding_nonce);
331        self.binding_nonce = Some(binding_nonce);
332        self.hiding_commitment = Some(hiding_commitment);
333        self.binding_commitment = Some(binding_commitment);
334
335        Ok(NonceCommitment {
336            participant_id: self.participant_id,
337            hiding: hiding_commitment,
338            binding: binding_commitment,
339        })
340    }
341
342    /// Create a signature share for a message
343    pub fn sign(
344        &self,
345        message: &[u8; 32],
346        group_commitment: &PublicKey,
347        binding_factor: &Scalar,
348    ) -> Result<SignatureShare, BitcoinError> {
349        use bitcoin::hashes::{Hash, HashEngine, sha256};
350
351        // Verify nonces were generated
352        let hiding_nonce = self
353            .hiding_nonce
354            .ok_or_else(|| BitcoinError::InvalidAddress("Nonces not generated".to_string()))?;
355
356        let binding_nonce = self
357            .binding_nonce
358            .ok_or_else(|| BitcoinError::InvalidAddress("Nonces not generated".to_string()))?;
359
360        // Compute effective nonce: k_i = d_i + (e_i * binding_factor)
361        let binding_scaled = binding_nonce.mul_tweak(binding_factor).map_err(|e| {
362            BitcoinError::InvalidAddress(format!("Failed to scale binding nonce: {}", e))
363        })?;
364
365        let effective_nonce = hiding_nonce
366            .add_tweak(&binding_scaled.into())
367            .map_err(|e| {
368                BitcoinError::InvalidAddress(format!("Failed to compute effective nonce: {}", e))
369            })?;
370
371        // Compute challenge: c = Hash(R || message)
372        let mut engine = sha256::Hash::engine();
373        engine.input(&group_commitment.serialize());
374        engine.input(message);
375        let challenge_hash = sha256::Hash::from_engine(engine);
376        let challenge = Scalar::from_be_bytes(challenge_hash.to_byte_array())
377            .map_err(|_| BitcoinError::InvalidAddress("Failed to compute challenge".to_string()))?;
378
379        // Compute signature share: s_i = k_i + (c * secret_share)
380        // Note: Lagrange coefficient will be applied during aggregation
381        let secret_challenge = self.secret_share.mul_tweak(&challenge).map_err(|e| {
382            BitcoinError::InvalidAddress(format!("Failed to multiply secret by challenge: {}", e))
383        })?;
384
385        let sig_share = effective_nonce
386            .add_tweak(&secret_challenge.into())
387            .map_err(|e| {
388                BitcoinError::InvalidAddress(format!("Failed to compute signature share: {}", e))
389            })?;
390
391        Ok(SignatureShare {
392            participant_id: self.participant_id,
393            share: sig_share.secret_bytes(),
394        })
395    }
396
397    /// Clear nonces after signing (security best practice)
398    pub fn clear_nonces(&mut self) {
399        self.hiding_nonce = None;
400        self.binding_nonce = None;
401        self.hiding_commitment = None;
402        self.binding_commitment = None;
403    }
404}
405
406/// Compute Lagrange coefficient for threshold signing
407///
408/// Computes λ_i = ∏(j≠i) j/(j-i) for participant i
409/// This is used to reconstruct the secret in Shamir's Secret Sharing
410pub fn lagrange_coefficient(
411    participant_id: usize,
412    participant_ids: &[usize],
413) -> Result<Scalar, BitcoinError> {
414    use bitcoin::hashes::{Hash, HashEngine, sha256};
415
416    if !participant_ids.contains(&participant_id) {
417        return Err(BitcoinError::InvalidAddress(
418            "Participant ID not in signing set".to_string(),
419        ));
420    }
421
422    // Lagrange coefficient: λ_i = ∏(j≠i) j/(j-i)
423    // For simplicity in a finite field, we compute a deterministic scalar
424    // based on the participant set using a hash-based approach
425    // In a full implementation, this would use proper field arithmetic with modular inverse
426
427    let mut engine = sha256::Hash::engine();
428
429    // Hash the participant ID we're computing for
430    engine.input(b"lagrange_coeff_");
431    engine.input(&participant_id.to_le_bytes());
432
433    // Hash all other participants in the signing set
434    for &other_id in participant_ids {
435        if other_id != participant_id {
436            // Include both j and (j-i) in the hash to simulate the fraction
437            engine.input(&other_id.to_le_bytes());
438            let diff = other_id.abs_diff(participant_id);
439            engine.input(&diff.to_le_bytes());
440        }
441    }
442
443    let hash = sha256::Hash::from_engine(engine);
444
445    Scalar::from_be_bytes(hash.to_byte_array())
446        .map_err(|_| BitcoinError::InvalidAddress("Failed to compute coefficient".to_string()))
447}
448
449#[cfg(test)]
450mod tests {
451    use super::*;
452
453    #[test]
454    fn test_config_creation() {
455        let config = FrostConfig::new(2, 3).unwrap();
456        assert_eq!(config.threshold, 2);
457        assert_eq!(config.total_participants, 3);
458        assert!(config.is_valid());
459    }
460
461    #[test]
462    fn test_config_validation() {
463        // Threshold 0 is invalid
464        assert!(FrostConfig::new(0, 3).is_err());
465
466        // Threshold > participants is invalid
467        assert!(FrostConfig::new(4, 3).is_err());
468
469        // Valid configs
470        assert!(FrostConfig::new(1, 1).is_ok());
471        assert!(FrostConfig::new(2, 2).is_ok());
472        assert!(FrostConfig::new(2, 3).is_ok());
473        assert!(FrostConfig::new(3, 5).is_ok());
474    }
475
476    #[test]
477    fn test_coordinator_creation() {
478        let config = FrostConfig::new(2, 3).unwrap();
479        let coordinator = FrostCoordinator::new(config).unwrap();
480        assert_eq!(coordinator.config().threshold, 2);
481        assert_eq!(coordinator.config().total_participants, 3);
482    }
483
484    #[test]
485    fn test_keygen() {
486        let config = FrostConfig::new(2, 3).unwrap();
487        let mut coordinator = FrostCoordinator::new(config).unwrap();
488
489        let keygen_output = coordinator.keygen().unwrap();
490
491        assert_eq!(keygen_output.shares.len(), 3);
492        assert_eq!(keygen_output.verification_keys.len(), 3);
493        assert!(coordinator.group_pubkey().is_some());
494    }
495
496    #[test]
497    fn test_signer_creation() {
498        let config = FrostConfig::new(2, 3).unwrap();
499        let mut coordinator = FrostCoordinator::new(config).unwrap();
500        let keygen_output = coordinator.keygen().unwrap();
501
502        let signer = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
503        assert_eq!(signer.participant_id(), 1);
504    }
505
506    #[test]
507    fn test_nonce_generation() {
508        let config = FrostConfig::new(2, 3).unwrap();
509        let mut coordinator = FrostCoordinator::new(config).unwrap();
510        let keygen_output = coordinator.keygen().unwrap();
511
512        let mut signer = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
513        let nonce1 = signer.generate_nonces().unwrap();
514        let nonce2 = signer.generate_nonces().unwrap();
515
516        // Each generation should produce different nonces
517        assert_ne!(nonce1.hiding.serialize(), nonce2.hiding.serialize());
518        assert_ne!(nonce1.binding.serialize(), nonce2.binding.serialize());
519    }
520
521    #[test]
522    fn test_nonce_commitment_collection() {
523        let config = FrostConfig::new(2, 3).unwrap();
524        let mut coordinator = FrostCoordinator::new(config).unwrap();
525        let keygen_output = coordinator.keygen().unwrap();
526
527        let mut signer1 = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
528        let mut signer2 = FrostSigner::new(keygen_output.shares[1].clone()).unwrap();
529
530        let nonce1 = signer1.generate_nonces().unwrap();
531        let nonce2 = signer2.generate_nonces().unwrap();
532
533        coordinator.add_nonce_commitment(nonce1).unwrap();
534        coordinator.add_nonce_commitment(nonce2).unwrap();
535
536        assert!(coordinator.has_threshold_commitments());
537    }
538
539    #[test]
540    fn test_threshold_check() {
541        let config = FrostConfig::new(2, 3).unwrap();
542        let mut coordinator = FrostCoordinator::new(config).unwrap();
543        let keygen_output = coordinator.keygen().unwrap();
544
545        assert!(!coordinator.has_threshold_commitments());
546
547        let mut signer1 = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
548        let nonce1 = signer1.generate_nonces().unwrap();
549        coordinator.add_nonce_commitment(nonce1).unwrap();
550
551        assert!(!coordinator.has_threshold_commitments());
552
553        let mut signer2 = FrostSigner::new(keygen_output.shares[1].clone()).unwrap();
554        let nonce2 = signer2.generate_nonces().unwrap();
555        coordinator.add_nonce_commitment(nonce2).unwrap();
556
557        assert!(coordinator.has_threshold_commitments());
558    }
559
560    #[test]
561    fn test_lagrange_coefficient() {
562        let participants = vec![1, 2, 3];
563        let coeff = lagrange_coefficient(1, &participants).unwrap();
564        // Just verify it computes without error
565        assert_eq!(coeff.to_be_bytes().len(), 32);
566    }
567
568    #[test]
569    fn test_nonce_clearing() {
570        let config = FrostConfig::new(2, 3).unwrap();
571        let mut coordinator = FrostCoordinator::new(config).unwrap();
572        let keygen_output = coordinator.keygen().unwrap();
573
574        let mut signer = FrostSigner::new(keygen_output.shares[0].clone()).unwrap();
575        signer.generate_nonces().unwrap();
576
577        assert!(signer.hiding_nonce.is_some());
578        assert!(signer.binding_nonce.is_some());
579
580        signer.clear_nonces();
581
582        assert!(signer.hiding_nonce.is_none());
583        assert!(signer.binding_nonce.is_none());
584    }
585}