chie_crypto/
dkg.rs

1//! Distributed Key Generation (DKG) using Feldman's VSS.
2//!
3//! This module provides distributed key generation without a trusted dealer.
4//! Multiple parties can jointly generate a shared secret key where no single
5//! party knows the full key, but any threshold of parties can reconstruct it.
6//!
7//! # Features
8//!
9//! - Feldman's Verifiable Secret Sharing (VSS)
10//! - Joint public key derivation
11//! - Threshold secret sharing (M-of-N)
12//! - Verification of shares without revealing them
13//!
14//! # Use Cases in CHIE Protocol
15//!
16//! - Decentralized coordinator setup
17//! - Threshold signing for governance
18//! - Distributed key management
19//!
20//! # Example
21//!
22//! ```
23//! use chie_crypto::dkg::{DkgParams, DkgParticipant, aggregate_public_key};
24//!
25//! // Setup: 3 participants, threshold of 2
26//! let params = DkgParams::new(3, 2);
27//!
28//! // Each participant generates their contribution
29//! let mut participants: Vec<_> = (0..3)
30//!     .map(|i| DkgParticipant::new(&params, i))
31//!     .collect();
32//!
33//! // Broadcast phase: each participant shares their commitments
34//! let commitments: Vec<_> = participants
35//!     .iter()
36//!     .map(|p| p.get_commitments())
37//!     .collect();
38//!
39//! // Each participant receives shares from others
40//! for i in 0..3 {
41//!     for j in 0..3 {
42//!         if i != j {
43//!             let share = participants[j].generate_share(i).unwrap();
44//!             participants[i].receive_share(j, share, &commitments[j]).unwrap();
45//!         }
46//!     }
47//! }
48//!
49//! // Compute joint public key
50//! let public_key = aggregate_public_key(&commitments);
51//! println!("Joint public key generated!");
52//! ```
53
54use curve25519_dalek::constants::RISTRETTO_BASEPOINT_POINT;
55use curve25519_dalek::ristretto::RistrettoPoint;
56use curve25519_dalek::scalar::Scalar;
57use curve25519_dalek::traits::Identity;
58use rand::Rng;
59use serde::{Deserialize, Serialize};
60use thiserror::Error;
61
62// Helper to generate random scalar
63fn random_scalar() -> Scalar {
64    let mut rng = rand::thread_rng();
65    let mut bytes = [0u8; 32];
66    rng.fill(&mut bytes);
67    Scalar::from_bytes_mod_order(bytes)
68}
69
70// Helper to generate random point
71#[allow(dead_code)]
72fn random_point() -> RistrettoPoint {
73    RISTRETTO_BASEPOINT_POINT * random_scalar()
74}
75
76/// DKG-specific errors.
77#[derive(Error, Debug)]
78pub enum DkgError {
79    #[error("Invalid threshold: must have 1 <= threshold <= total_parties")]
80    InvalidThreshold,
81    #[error("Invalid participant ID")]
82    InvalidParticipantId,
83    #[error("Invalid share")]
84    InvalidShare,
85    #[error("Share verification failed")]
86    ShareVerificationFailed,
87    #[error("Insufficient shares for reconstruction")]
88    InsufficientShares,
89    #[error("Duplicate participant ID")]
90    DuplicateParticipant,
91}
92
93pub type DkgResult<T> = Result<T, DkgError>;
94
95/// Parameters for DKG protocol.
96#[derive(Clone, Debug)]
97pub struct DkgParams {
98    /// Total number of participants
99    pub total_parties: usize,
100    /// Threshold: minimum parties needed to reconstruct secret
101    pub threshold: usize,
102    /// Generator point
103    g: RistrettoPoint,
104}
105
106impl DkgParams {
107    /// Create new DKG parameters.
108    ///
109    /// # Arguments
110    ///
111    /// * `total_parties` - Total number of participants
112    /// * `threshold` - Minimum number of parties needed to reconstruct
113    ///
114    /// # Example
115    ///
116    /// ```
117    /// use chie_crypto::dkg::DkgParams;
118    ///
119    /// // 5 parties, threshold of 3
120    /// let params = DkgParams::new(5, 3);
121    /// assert_eq!(params.total_parties, 5);
122    /// assert_eq!(params.threshold, 3);
123    /// ```
124    pub fn new(total_parties: usize, threshold: usize) -> Self {
125        assert!(threshold > 0 && threshold <= total_parties);
126
127        // Use standard Ristretto basepoint for compatibility with other protocols (e.g., FROST)
128        let g = RISTRETTO_BASEPOINT_POINT;
129
130        Self {
131            total_parties,
132            threshold,
133            g,
134        }
135    }
136}
137
138/// A participant in the DKG protocol.
139pub struct DkgParticipant {
140    /// Participant's ID (0-indexed)
141    id: usize,
142    /// DKG parameters
143    params: DkgParams,
144    /// Secret polynomial coefficients (a_0, a_1, ..., a_{t-1})
145    coefficients: Vec<Scalar>,
146    /// Commitments to polynomial coefficients (g^a_0, g^a_1, ...)
147    commitments: Vec<RistrettoPoint>,
148    /// Shares received from other participants
149    received_shares: Vec<Option<Scalar>>,
150}
151
152/// Public commitments broadcast by a participant.
153#[derive(Clone, Debug, Serialize, Deserialize)]
154pub struct DkgCommitments {
155    /// Participant ID
156    pub participant_id: usize,
157    /// Commitments to polynomial coefficients
158    pub commitments: Vec<Vec<u8>>, // Compressed Ristretto points
159}
160
161/// A secret share for a participant.
162#[derive(Clone, Debug, Serialize, Deserialize)]
163pub struct DkgShare {
164    /// Scalar value of the share
165    value: Vec<u8>, // Scalar bytes
166}
167
168impl DkgParticipant {
169    /// Create a new DKG participant.
170    ///
171    /// # Arguments
172    ///
173    /// * `params` - DKG parameters
174    /// * `id` - Participant's ID (0-indexed)
175    pub fn new(params: &DkgParams, id: usize) -> Self {
176        assert!(id < params.total_parties);
177
178        // Generate random polynomial of degree threshold-1
179        // f(x) = a_0 + a_1*x + a_2*x^2 + ... + a_{t-1}*x^{t-1}
180        let coefficients: Vec<Scalar> = (0..params.threshold).map(|_| random_scalar()).collect();
181
182        // Compute commitments: C_i = g^a_i
183        let commitments: Vec<RistrettoPoint> =
184            coefficients.iter().map(|coeff| params.g * coeff).collect();
185
186        let received_shares = vec![None; params.total_parties];
187
188        Self {
189            id,
190            params: params.clone(),
191            coefficients,
192            commitments,
193            received_shares,
194        }
195    }
196
197    /// Get public commitments to broadcast.
198    pub fn get_commitments(&self) -> DkgCommitments {
199        DkgCommitments {
200            participant_id: self.id,
201            commitments: self
202                .commitments
203                .iter()
204                .map(|c| c.compress().as_bytes().to_vec())
205                .collect(),
206        }
207    }
208
209    /// Generate a share for participant `target_id`.
210    ///
211    /// # Arguments
212    ///
213    /// * `target_id` - ID of the participant to receive this share
214    pub fn generate_share(&self, target_id: usize) -> DkgResult<DkgShare> {
215        if target_id >= self.params.total_parties {
216            return Err(DkgError::InvalidParticipantId);
217        }
218
219        // Evaluate polynomial at x = target_id + 1
220        // (We add 1 to avoid evaluating at 0)
221        let x = Scalar::from((target_id + 1) as u64);
222        let share_value = evaluate_polynomial(&self.coefficients, x);
223
224        Ok(DkgShare {
225            value: share_value.to_bytes().to_vec(),
226        })
227    }
228
229    /// Receive and verify a share from another participant.
230    ///
231    /// # Arguments
232    ///
233    /// * `from_id` - ID of the sending participant
234    /// * `share` - The share received
235    /// * `commitments` - Public commitments from the sender
236    pub fn receive_share(
237        &mut self,
238        from_id: usize,
239        share: DkgShare,
240        commitments: &DkgCommitments,
241    ) -> DkgResult<()> {
242        if from_id >= self.params.total_parties {
243            return Err(DkgError::InvalidParticipantId);
244        }
245
246        if commitments.participant_id != from_id {
247            return Err(DkgError::InvalidShare);
248        }
249
250        if self.received_shares[from_id].is_some() {
251            return Err(DkgError::DuplicateParticipant);
252        }
253
254        // Deserialize share
255        if share.value.len() != 32 {
256            return Err(DkgError::InvalidShare);
257        }
258        let mut share_bytes = [0u8; 32];
259        share_bytes.copy_from_slice(&share.value);
260        let share_scalar = Scalar::from_bytes_mod_order(share_bytes);
261
262        // Verify share using commitments
263        // Check: g^share == C_0 * C_1^x * C_2^x^2 * ... * C_{t-1}^x^{t-1}
264        let x = Scalar::from((self.id + 1) as u64);
265
266        let mut expected = RistrettoPoint::identity();
267        let mut x_power = Scalar::ONE;
268
269        for commitment_bytes in &commitments.commitments {
270            if commitment_bytes.len() != 32 {
271                return Err(DkgError::InvalidShare);
272            }
273
274            let mut bytes = [0u8; 32];
275            bytes.copy_from_slice(commitment_bytes);
276
277            let commitment = curve25519_dalek::ristretto::CompressedRistretto(bytes)
278                .decompress()
279                .ok_or(DkgError::InvalidShare)?;
280
281            expected += commitment * x_power;
282            x_power *= x;
283        }
284
285        let actual = self.params.g * share_scalar;
286
287        if actual != expected {
288            return Err(DkgError::ShareVerificationFailed);
289        }
290
291        // Store verified share
292        self.received_shares[from_id] = Some(share_scalar);
293
294        Ok(())
295    }
296
297    /// Get the participant's secret share (sum of received shares + own share).
298    ///
299    /// This should only be called after receiving shares from all other participants.
300    pub fn get_secret_share(&self) -> DkgResult<Scalar> {
301        // Add own share
302        let own_share = self.generate_share(self.id)?;
303        let mut own_bytes = [0u8; 32];
304        own_bytes.copy_from_slice(&own_share.value);
305        let mut total = Scalar::from_bytes_mod_order(own_bytes);
306
307        // Add received shares
308        for share in self.received_shares.iter().flatten() {
309            total += share;
310        }
311
312        Ok(total)
313    }
314}
315
316/// Aggregate public keys from all participants to get joint public key.
317///
318/// # Arguments
319///
320/// * `all_commitments` - Commitments from all participants
321///
322/// # Example
323///
324/// ```
325/// use chie_crypto::dkg::{DkgParams, DkgParticipant, aggregate_public_key};
326///
327/// let params = DkgParams::new(3, 2);
328/// let participants: Vec<_> = (0..3)
329///     .map(|i| DkgParticipant::new(&params, i))
330///     .collect();
331///
332/// let commitments: Vec<_> = participants
333///     .iter()
334///     .map(|p| p.get_commitments())
335///     .collect();
336///
337/// let public_key = aggregate_public_key(&commitments);
338/// ```
339pub fn aggregate_public_key(all_commitments: &[DkgCommitments]) -> RistrettoPoint {
340    // Joint public key is the sum of all first commitments (C_0 from each party)
341    let mut joint_pk = RistrettoPoint::identity();
342
343    for commitments in all_commitments {
344        if !commitments.commitments.is_empty() {
345            let mut bytes = [0u8; 32];
346            bytes.copy_from_slice(&commitments.commitments[0]);
347
348            if let Some(point) =
349                curve25519_dalek::ristretto::CompressedRistretto(bytes).decompress()
350            {
351                joint_pk += point;
352            }
353        }
354    }
355
356    joint_pk
357}
358
359// Helper: Evaluate polynomial at point x
360fn evaluate_polynomial(coefficients: &[Scalar], x: Scalar) -> Scalar {
361    let mut result = Scalar::ZERO;
362    let mut x_power = Scalar::ONE;
363
364    for coeff in coefficients {
365        result += coeff * x_power;
366        x_power *= x;
367    }
368
369    result
370}
371
372#[cfg(test)]
373mod tests {
374    use super::*;
375
376    #[test]
377    fn test_dkg_basic() {
378        let params = DkgParams::new(3, 2);
379
380        let mut participants: Vec<_> = (0..3).map(|i| DkgParticipant::new(&params, i)).collect();
381
382        // Broadcast commitments
383        let commitments: Vec<_> = participants.iter().map(|p| p.get_commitments()).collect();
384
385        // Distribute shares
386        for i in 0..3 {
387            for j in 0..3 {
388                if i != j {
389                    let share = participants[j].generate_share(i).unwrap();
390                    participants[i]
391                        .receive_share(j, share, &commitments[j])
392                        .unwrap();
393                }
394            }
395        }
396
397        // Each participant gets their secret share
398        let shares: Vec<_> = participants
399            .iter()
400            .map(|p| p.get_secret_share().unwrap())
401            .collect();
402
403        assert_eq!(shares.len(), 3);
404    }
405
406    #[test]
407    fn test_dkg_aggregate_public_key() {
408        let params = DkgParams::new(5, 3);
409
410        let participants: Vec<_> = (0..5).map(|i| DkgParticipant::new(&params, i)).collect();
411
412        let commitments: Vec<_> = participants.iter().map(|p| p.get_commitments()).collect();
413
414        let public_key = aggregate_public_key(&commitments);
415
416        // Public key should not be identity
417        assert_ne!(public_key, RistrettoPoint::identity());
418    }
419
420    #[test]
421    fn test_dkg_invalid_threshold() {
422        let params = DkgParams::new(3, 2);
423        let participant = DkgParticipant::new(&params, 0);
424
425        // Try to generate share for invalid participant
426        assert!(participant.generate_share(10).is_err());
427    }
428
429    #[test]
430    fn test_dkg_share_verification() {
431        let params = DkgParams::new(3, 2);
432        let mut participant0 = DkgParticipant::new(&params, 0);
433        let participant1 = DkgParticipant::new(&params, 1);
434
435        let commitments1 = participant1.get_commitments();
436        let share = participant1.generate_share(0).unwrap();
437
438        // Should verify correctly
439        assert!(
440            participant0
441                .receive_share(1, share.clone(), &commitments1)
442                .is_ok()
443        );
444
445        // Should reject duplicate
446        assert!(participant0.receive_share(1, share, &commitments1).is_err());
447    }
448
449    #[test]
450    fn test_dkg_different_thresholds() {
451        for (total, threshold) in [(3, 2), (5, 3), (7, 4)] {
452            let params = DkgParams::new(total, threshold);
453
454            let mut participants: Vec<_> = (0..total)
455                .map(|i| DkgParticipant::new(&params, i))
456                .collect();
457
458            let commitments: Vec<_> = participants.iter().map(|p| p.get_commitments()).collect();
459
460            // Distribute shares
461            for i in 0..total {
462                for j in 0..total {
463                    if i != j {
464                        let share = participants[j].generate_share(i).unwrap();
465                        participants[i]
466                            .receive_share(j, share, &commitments[j])
467                            .unwrap();
468                    }
469                }
470            }
471
472            // Verify all participants can get their shares
473            for p in &participants {
474                assert!(p.get_secret_share().is_ok());
475            }
476
477            // Verify joint public key
478            let pk = aggregate_public_key(&commitments);
479            assert_ne!(pk, RistrettoPoint::identity());
480        }
481    }
482
483    #[test]
484    fn test_evaluate_polynomial() {
485        let coefficients = vec![
486            Scalar::from(1u64), // constant term
487            Scalar::from(2u64), // x term
488            Scalar::from(3u64), // x^2 term
489        ];
490
491        // Evaluate at x=2: 1 + 2*2 + 3*4 = 1 + 4 + 12 = 17
492        let x = Scalar::from(2u64);
493        let result = evaluate_polynomial(&coefficients, x);
494        let expected = Scalar::from(17u64);
495
496        assert_eq!(result, expected);
497    }
498
499    #[test]
500    fn test_dkg_partial_shares() {
501        let params = DkgParams::new(5, 3);
502        let mut participants: Vec<_> = (0..5).map(|i| DkgParticipant::new(&params, i)).collect();
503
504        let commitments: Vec<_> = participants.iter().map(|p| p.get_commitments()).collect();
505
506        // Only distribute shares from 2 participants (less than threshold of 3)
507        for i in 0..5 {
508            for j in 0..2 {
509                if i != j {
510                    let share = participants[j].generate_share(i).unwrap();
511                    participants[i]
512                        .receive_share(j, share, &commitments[j])
513                        .unwrap();
514                }
515            }
516        }
517
518        // Participant can still compute a share, but it won't be the full secret
519        // This demonstrates that the protocol requires all participants
520        let result = participants[0].get_secret_share();
521        assert!(result.is_ok());
522
523        // The computed share exists but would be different with all participants
524        let partial_share = result.unwrap();
525        assert_ne!(partial_share, Scalar::ZERO);
526    }
527
528    #[test]
529    fn test_dkg_serialization() {
530        let params = DkgParams::new(3, 2);
531        let participant = DkgParticipant::new(&params, 0);
532
533        let commitments = participant.get_commitments();
534
535        // Serialize
536        let serialized = crate::codec::encode(&commitments).unwrap();
537
538        // Deserialize
539        let deserialized: DkgCommitments = crate::codec::decode(&serialized).unwrap();
540
541        assert_eq!(
542            commitments.commitments.len(),
543            deserialized.commitments.len()
544        );
545        for (orig, deser) in commitments
546            .commitments
547            .iter()
548            .zip(deserialized.commitments.iter())
549        {
550            assert_eq!(orig, deser);
551        }
552    }
553
554    #[test]
555    fn test_dkg_invalid_share_verification() {
556        let params = DkgParams::new(3, 2);
557        let mut participant0 = DkgParticipant::new(&params, 0);
558        let participant1 = DkgParticipant::new(&params, 1);
559
560        let commitments1 = participant1.get_commitments();
561
562        // Generate a valid share for participant 0
563        let valid_share = participant1.generate_share(0).unwrap();
564
565        // Create an invalid share by corrupting the value
566        let mut corrupted_value = valid_share.value.clone();
567        corrupted_value[0] = corrupted_value[0].wrapping_add(1); // Corrupt first byte
568        let invalid_share = DkgShare {
569            value: corrupted_value,
570        };
571
572        // Should reject invalid share
573        let result = participant0.receive_share(1, invalid_share, &commitments1);
574        assert!(result.is_err());
575        assert!(matches!(result, Err(DkgError::ShareVerificationFailed)));
576    }
577
578    #[test]
579    fn test_dkg_commitment_consistency() {
580        let params = DkgParams::new(3, 2);
581
582        // Create multiple participants
583        let participants: Vec<_> = (0..3).map(|i| DkgParticipant::new(&params, i)).collect();
584
585        // Get commitments from each participant
586        let commitments: Vec<_> = participants.iter().map(|p| p.get_commitments()).collect();
587
588        // Verify all commitments have correct length (threshold)
589        for commitment in &commitments {
590            assert_eq!(commitment.commitments.len(), params.threshold);
591        }
592
593        // Verify commitment points are not identity (identity point has specific bytes)
594        let identity_bytes = RistrettoPoint::identity().compress().to_bytes();
595        for commitment in &commitments {
596            for point_bytes in &commitment.commitments {
597                assert_ne!(point_bytes.as_slice(), identity_bytes.as_slice());
598            }
599        }
600
601        // Verify joint public key is deterministic from same commitments
602        let pk1 = aggregate_public_key(&commitments);
603        let pk2 = aggregate_public_key(&commitments);
604        assert_eq!(pk1, pk2);
605    }
606}