Skip to main content

exo_root/
dkg.rs

1//! FROST DKG wrappers for root genesis.
2
3use std::{collections::BTreeMap, fmt::Display};
4
5use frost_ristretto255 as frost;
6use serde::{Deserialize, Serialize, de::DeserializeOwned};
7
8use crate::{GenesisCeremonyConfig, Result, RootError};
9
10/// Serialized public key package and derived public metadata.
11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct RootPublicKeyPackage {
13    /// Serialized FROST public key package.
14    pub public_key_package: Vec<u8>,
15    /// Serialized root verifying key.
16    pub root_public_key: Vec<u8>,
17    /// Serialized verification shares by FROST identifier.
18    pub verifying_shares: BTreeMap<u16, Vec<u8>>,
19}
20
21/// Serialized FROST key package held by one certifier.
22#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct RootKeyPackage {
24    /// Owner's FROST identifier.
25    pub frost_identifier: u16,
26    /// Serialized FROST key package.
27    pub key_package: Vec<u8>,
28}
29
30/// Complete in-memory DKG result for tests and offline ceremony tooling.
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct RootDkgOutput {
33    /// Certifier key packages by FROST identifier.
34    pub key_packages: BTreeMap<u16, RootKeyPackage>,
35    /// Public key package common to all certifiers.
36    pub public_key_package: RootPublicKeyPackage,
37}
38
39/// Serialized output from one certifier's DKG round one.
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct RootDkgRound1Output {
42    /// Owner's FROST identifier.
43    pub frost_identifier: u16,
44    /// Private round-one state retained by the certifier.
45    pub round1_secret_package: Vec<u8>,
46    /// Public round-one package broadcast to every other certifier.
47    pub round1_package: Vec<u8>,
48}
49
50/// Serialized output from one certifier's DKG round two.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct RootDkgRound2Output {
53    /// Owner's FROST identifier.
54    pub frost_identifier: u16,
55    /// Private round-two state retained by the certifier.
56    pub round2_secret_package: Vec<u8>,
57    /// Recipient-bound round-two packages by recipient FROST identifier.
58    pub round2_packages: BTreeMap<u16, Vec<u8>>,
59}
60
61/// Final DKG material derived by one certifier.
62#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct RootParticipantDkgOutput {
64    /// Owner's FROST key package.
65    pub key_package: RootKeyPackage,
66    /// Public root key package derived by the participant.
67    pub public_key_package: RootPublicKeyPackage,
68}
69
70pub(crate) fn frost_identifier(identifier: u16) -> Result<frost::Identifier> {
71    frost::Identifier::try_from(identifier).map_err(frost_error)
72}
73
74fn rostered_frost_identifier(
75    config: &GenesisCeremonyConfig,
76    identifier: u16,
77    operation: &str,
78) -> Result<frost::Identifier> {
79    if config.certifier_by_identifier(identifier).is_none() {
80        return Err(RootError::InvalidConfig {
81            reason: format!("{operation} certifier {identifier} is not rostered"),
82        });
83    }
84    frost_identifier(identifier)
85}
86
87fn frost_error(error: frost::Error) -> RootError {
88    RootError::Frost {
89        detail: error.to_string(),
90    }
91}
92
93fn frost_encoding_error(error: impl Display) -> RootError {
94    RootError::Frost {
95        detail: format!("FROST artifact canonical encoding failed: {error}"),
96    }
97}
98
99pub(crate) fn serialize_frost<T: Serialize>(value: &T) -> Result<Vec<u8>> {
100    let mut bytes = Vec::new();
101    ciborium::into_writer(value, &mut bytes).map_err(frost_encoding_error)?;
102    Ok(bytes)
103}
104
105pub(crate) fn deserialize_frost<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
106    ciborium::from_reader(bytes).map_err(frost_encoding_error)
107}
108
109fn identifier_value(
110    config: &GenesisCeremonyConfig,
111    frost_identifier: frost::Identifier,
112) -> Result<u16> {
113    for certifier in &config.certifiers {
114        if crate::dkg::frost_identifier(certifier.frost_identifier)? == frost_identifier {
115            return Ok(certifier.frost_identifier);
116        }
117    }
118    Err(RootError::Frost {
119        detail: "FROST identifier is not rostered".to_owned(),
120    })
121}
122
123fn peer_packages_except(
124    packages: &BTreeMap<u16, Vec<u8>>,
125    excluded: u16,
126) -> BTreeMap<u16, Vec<u8>> {
127    packages
128        .iter()
129        .filter(|(peer, _)| **peer != excluded)
130        .map(|(peer, package)| (*peer, package.clone()))
131        .collect()
132}
133
134fn frost_dkg_round1<R>(
135    identifier: frost::Identifier,
136    max_signers: u16,
137    threshold: u16,
138    rng: &mut R,
139) -> Result<(
140    frost::keys::dkg::round1::SecretPackage,
141    frost::keys::dkg::round1::Package,
142)>
143where
144    R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
145{
146    frost::keys::dkg::part1(identifier, max_signers, threshold, rng).map_err(frost_error)
147}
148
149pub(crate) fn serialize_public_key_package(
150    config: &GenesisCeremonyConfig,
151    package: &frost::keys::PublicKeyPackage,
152) -> Result<RootPublicKeyPackage> {
153    let public_key_package = serialize_frost(package)?;
154    let root_public_key = serialize_frost(package.verifying_key())?;
155    let mut verifying_shares = BTreeMap::new();
156    for certifier in &config.certifiers {
157        let identifier = frost_identifier(certifier.frost_identifier)?;
158        let share =
159            package
160                .verifying_shares()
161                .get(&identifier)
162                .ok_or_else(|| RootError::Frost {
163                    detail: format!(
164                        "missing verification share for identifier {}",
165                        certifier.frost_identifier
166                    ),
167                })?;
168        verifying_shares.insert(certifier.frost_identifier, serialize_frost(share)?);
169    }
170    Ok(RootPublicKeyPackage {
171        public_key_package,
172        root_public_key,
173        verifying_shares,
174    })
175}
176
177pub(crate) fn validate_public_key_package(
178    config: &GenesisCeremonyConfig,
179    package: &RootPublicKeyPackage,
180) -> Result<()> {
181    let frost_package: frost::keys::PublicKeyPackage =
182        deserialize_frost(package.public_key_package.as_slice())?;
183    let expected = serialize_public_key_package(config, &frost_package)?;
184    if &expected != package {
185        return Err(RootError::BundleRejected {
186            reason: "public key package metadata does not match serialized FROST package"
187                .to_owned(),
188        });
189    }
190    Ok(())
191}
192
193/// Execute DKG round one for one rostered certifier.
194pub fn dkg_round1<R>(
195    config: &GenesisCeremonyConfig,
196    frost_identifier_value: u16,
197    rng: &mut R,
198) -> Result<RootDkgRound1Output>
199where
200    R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
201{
202    config.validate()?;
203    let identifier = rostered_frost_identifier(config, frost_identifier_value, "round-one")?;
204    let max_signers = config.max_signers;
205    let threshold = config.threshold;
206    let round1 = frost_dkg_round1(identifier, max_signers, threshold, rng)?;
207    let (secret_package, package) = round1;
208    let output = RootDkgRound1Output {
209        frost_identifier: frost_identifier_value,
210        round1_secret_package: serialize_frost(&secret_package)?,
211        round1_package: serialize_frost(&package)?,
212    };
213    Ok(output)
214}
215
216/// Execute DKG round two for one certifier after all other round-one packages
217/// have been authenticated and collected.
218pub fn dkg_round2(
219    config: &GenesisCeremonyConfig,
220    frost_identifier_value: u16,
221    round1_secret_package: &[u8],
222    round1_packages: BTreeMap<u16, Vec<u8>>,
223) -> Result<RootDkgRound2Output> {
224    config.validate()?;
225    if round1_packages.len() != usize::from(config.max_signers - 1) {
226        return Err(RootError::Frost {
227            detail: "round two requires all twelve peer round-one packages".to_owned(),
228        });
229    }
230    let participant_identifier =
231        rostered_frost_identifier(config, frost_identifier_value, "round-two")?;
232    let secret_package = deserialize_frost(round1_secret_package)?;
233    let inbound_round1 =
234        deserialize_round1_packages(config, participant_identifier, round1_packages)?;
235    let (round2_secret_package, outbound) =
236        frost::keys::dkg::part2(secret_package, &inbound_round1).map_err(frost_error)?;
237    let mut round2_packages = BTreeMap::new();
238    for (recipient, package) in outbound {
239        let recipient_value = identifier_value(config, recipient)?;
240        round2_packages.insert(recipient_value, serialize_frost(&package)?);
241    }
242    Ok(RootDkgRound2Output {
243        frost_identifier: frost_identifier_value,
244        round2_secret_package: serialize_frost(&round2_secret_package)?,
245        round2_packages,
246    })
247}
248
249/// Finalize one participant's DKG state after all peer round-one and round-two
250/// packages have been authenticated and collected.
251pub fn dkg_finalize_participant(
252    config: &GenesisCeremonyConfig,
253    frost_identifier_value: u16,
254    round2_secret_package: &[u8],
255    round1_packages: BTreeMap<u16, Vec<u8>>,
256    round2_packages: BTreeMap<u16, Vec<u8>>,
257) -> Result<RootParticipantDkgOutput> {
258    config.validate()?;
259    if round1_packages.len() != usize::from(config.max_signers - 1) {
260        return Err(RootError::Frost {
261            detail: "finalize requires all twelve peer round-one packages".to_owned(),
262        });
263    }
264    if round2_packages.len() != usize::from(config.max_signers - 1) {
265        return Err(RootError::Frost {
266            detail: "finalize requires all twelve peer round-two packages".to_owned(),
267        });
268    }
269    let participant_identifier =
270        rostered_frost_identifier(config, frost_identifier_value, "finalize")?;
271    let secret_package = deserialize_frost(round2_secret_package)?;
272    let inbound_round1 =
273        deserialize_round1_packages(config, participant_identifier, round1_packages)?;
274    let inbound_round2 =
275        deserialize_round2_packages(config, participant_identifier, round2_packages)?;
276    let (key_package, public_key_package) =
277        frost::keys::dkg::part3(&secret_package, &inbound_round1, &inbound_round2)
278            .map_err(frost_error)?;
279    let key_package = RootKeyPackage {
280        frost_identifier: frost_identifier_value,
281        key_package: serialize_frost(&key_package)?,
282    };
283    Ok(RootParticipantDkgOutput {
284        key_package,
285        public_key_package: serialize_public_key_package(config, &public_key_package)?,
286    })
287}
288
289fn deserialize_round1_packages(
290    config: &GenesisCeremonyConfig,
291    participant_identifier: frost::Identifier,
292    packages: BTreeMap<u16, Vec<u8>>,
293) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round1::Package>> {
294    let mut result = BTreeMap::new();
295    for (sender, package_bytes) in packages {
296        if config.certifier_by_identifier(sender).is_none() {
297            return Err(RootError::InvalidConfig {
298                reason: format!("round-one sender {sender} is not rostered"),
299            });
300        }
301        let sender_identifier = frost_identifier(sender)?;
302        if sender_identifier == participant_identifier {
303            return Err(RootError::Frost {
304                detail: "round-one peer packages must not include self".to_owned(),
305            });
306        }
307        let package = deserialize_frost(package_bytes.as_slice())?;
308        result.insert(sender_identifier, package);
309    }
310    Ok(result)
311}
312
313fn deserialize_round2_packages(
314    config: &GenesisCeremonyConfig,
315    participant_identifier: frost::Identifier,
316    packages: BTreeMap<u16, Vec<u8>>,
317) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round2::Package>> {
318    let mut result = BTreeMap::new();
319    for (sender, package_bytes) in packages {
320        if config.certifier_by_identifier(sender).is_none() {
321            return Err(RootError::InvalidConfig {
322                reason: format!("round-two sender {sender} is not rostered"),
323            });
324        }
325        let sender_identifier = frost_identifier(sender)?;
326        if sender_identifier == participant_identifier {
327            return Err(RootError::Frost {
328                detail: "round-two peer packages must not include self".to_owned(),
329            });
330        }
331        let package = deserialize_frost(package_bytes.as_slice())?;
332        result.insert(sender_identifier, package);
333    }
334    Ok(result)
335}
336
337/// Run the all-roster DKG ceremony locally.
338///
339/// Production ceremonies should exchange these packages through the portal and
340/// pairwise encrypted channels. This function enforces the same all-thirteen
341/// completion rule for deterministic regression tests and offline rehearsals.
342pub fn run_complete_dkg<R>(config: &GenesisCeremonyConfig, rng: &mut R) -> Result<RootDkgOutput>
343where
344    R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
345{
346    config.validate()?;
347
348    let mut round1_outputs = BTreeMap::new();
349    let mut round1_public = BTreeMap::new();
350    for certifier in &config.certifiers {
351        let output = dkg_round1(config, certifier.frost_identifier, rng)?;
352        round1_public.insert(certifier.frost_identifier, output.round1_package.clone());
353        round1_outputs.insert(certifier.frost_identifier, output);
354    }
355
356    let mut round2_outputs = BTreeMap::new();
357    let mut round2_by_recipient: BTreeMap<u16, BTreeMap<u16, Vec<u8>>> = BTreeMap::new();
358    for (identifier, round1_output) in &round1_outputs {
359        let peer_round1 = peer_packages_except(&round1_public, *identifier);
360        let secret = &round1_output.round1_secret_package;
361        let round2 = dkg_round2(config, *identifier, secret, peer_round1)?;
362        for (recipient, package) in &round2.round2_packages {
363            let recipient_packages = round2_by_recipient.entry(*recipient).or_default();
364            recipient_packages.insert(*identifier, package.clone());
365        }
366        round2_outputs.insert(*identifier, round2);
367    }
368
369    let mut key_packages = BTreeMap::new();
370    let finish = dkg_finalize_participant;
371    let first_identifier = config.certifiers[0].frost_identifier;
372    let output = &round2_outputs[&first_identifier];
373    let fr1 = peer_packages_except(&round1_public, first_identifier);
374    let fs = &output.round2_secret_package;
375    let fr2 = round2_by_recipient[&first_identifier].clone();
376    let first_participant = finish(config, first_identifier, fs, fr1, fr2)?;
377    let public_key_package = first_participant.public_key_package;
378    key_packages.insert(first_identifier, first_participant.key_package);
379
380    for (identifier, round2_output) in round2_outputs
381        .iter()
382        .filter(|(identifier, _)| **identifier != first_identifier)
383    {
384        let identifier = *identifier;
385        let peer_round1 = peer_packages_except(&round1_public, identifier);
386        let secret = &round2_output.round2_secret_package;
387        let round2 = round2_by_recipient[&identifier].clone();
388        let participant = finish(config, identifier, secret, peer_round1, round2)?;
389        key_packages.insert(identifier, participant.key_package);
390    }
391
392    Ok(RootDkgOutput {
393        key_packages,
394        public_key_package,
395    })
396}
397
398#[cfg(test)]
399mod tests {
400    use exo_core::{Did, Hash256, PublicKey, Timestamp};
401    use rand::{SeedableRng, rngs::StdRng};
402
403    use super::*;
404    use crate::CertifierContact;
405
406    fn test_config() -> GenesisCeremonyConfig {
407        let certifiers = (1..=13)
408            .map(|identifier| {
409                let byte = u8::try_from(identifier).expect("identifier fits");
410                CertifierContact {
411                    did: Did::new(&format!("did:exo:unit-{identifier:02}")).expect("valid did"),
412                    frost_identifier: identifier,
413                    signing_public_key: PublicKey::from_bytes([byte; 32]),
414                    transport_public_key: [byte; 32],
415                }
416            })
417            .collect();
418        GenesisCeremonyConfig {
419            ceremony_id: "unit-root".into(),
420            network_id: "unit-net".into(),
421            repo_commit: "d8927686a34bdc28ba36d53938f665685d2c4c04".into(),
422            constitution_hash: Hash256::digest(b"constitution"),
423            threshold: 7,
424            max_signers: 13,
425            created_at: Timestamp::new(1, 0),
426            certifiers,
427            signing_set: (1..=7).collect(),
428        }
429    }
430
431    #[test]
432    fn identifier_value_rejects_unrostered_identifier() {
433        let config = test_config();
434        let identifier = frost_identifier(14).expect("identifier");
435        assert!(identifier_value(&config, identifier).is_err());
436    }
437
438    #[test]
439    fn frost_identifier_rejects_zero_identifier() {
440        let error = frost_identifier(0).expect_err("zero identifier");
441        assert!(error.to_string().contains("frost operation failed"));
442    }
443
444    #[test]
445    fn rostered_identifier_and_encoding_helpers_are_diagnostic() {
446        let config = test_config();
447        let identifier = rostered_frost_identifier(&config, 1, "unit").expect("rostered");
448        assert_eq!(identifier, frost_identifier(1).expect("identifier"));
449
450        let error =
451            rostered_frost_identifier(&config, 14, "unit").expect_err("unrostered certifier");
452        assert!(
453            error
454                .to_string()
455                .contains("unit certifier 14 is not rostered")
456        );
457
458        let encoding_error = frost_encoding_error("unit failure");
459        assert!(
460            encoding_error
461                .to_string()
462                .contains("FROST artifact canonical encoding failed")
463        );
464
465        let encoded = serialize_frost(&7u16).expect("serialize");
466        let decoded: u16 = deserialize_frost(encoded.as_slice()).expect("deserialize");
467        assert_eq!(decoded, 7);
468        assert!(deserialize_frost::<u16>(b"not cbor").is_err());
469    }
470
471    #[test]
472    fn peer_package_filter_retains_every_non_excluded_package() {
473        let mut packages = BTreeMap::new();
474        packages.insert(1, b"one".to_vec());
475        packages.insert(2, b"two".to_vec());
476        packages.insert(3, b"three".to_vec());
477
478        let peers = peer_packages_except(&packages, 2);
479
480        assert_eq!(peers.len(), 2);
481        assert_eq!(peers.get(&1).expect("peer one"), b"one");
482        assert_eq!(peers.get(&3).expect("peer three"), b"three");
483        assert!(!peers.contains_key(&2));
484    }
485
486    #[test]
487    fn round_one_and_complete_dkg_success_paths_are_diagnostic() {
488        let config = test_config();
489        let mut rng = StdRng::seed_from_u64(11);
490
491        let round1 = dkg_round1(&config, 1, &mut rng).expect("round one");
492        assert_eq!(round1.frost_identifier, 1);
493        assert!(!round1.round1_secret_package.is_empty());
494        assert!(!round1.round1_package.is_empty());
495
496        let dkg = run_complete_dkg(&config, &mut rng).expect("complete dkg");
497        assert_eq!(dkg.key_packages.len(), usize::from(config.max_signers));
498        assert_eq!(
499            dkg.public_key_package.verifying_shares.len(),
500            usize::from(config.max_signers)
501        );
502    }
503
504    #[test]
505    fn deserialize_peer_package_helpers_reject_bad_sender_sets() {
506        let config = test_config();
507        let participant = frost_identifier(1).expect("participant");
508
509        assert!(
510            deserialize_round1_packages(&config, participant, BTreeMap::new())
511                .expect("empty round-one helper input")
512                .is_empty()
513        );
514        assert!(
515            deserialize_round2_packages(&config, participant, BTreeMap::new())
516                .expect("empty round-two helper input")
517                .is_empty()
518        );
519
520        let mut nonrostered_round1 = BTreeMap::new();
521        nonrostered_round1.insert(14, Vec::new());
522        assert!(deserialize_round1_packages(&config, participant, nonrostered_round1).is_err());
523
524        let mut self_round1 = BTreeMap::new();
525        self_round1.insert(1, Vec::new());
526        assert!(deserialize_round1_packages(&config, participant, self_round1).is_err());
527
528        let mut malformed_round1 = BTreeMap::new();
529        malformed_round1.insert(2, b"not a round-one package".to_vec());
530        assert!(deserialize_round1_packages(&config, participant, malformed_round1).is_err());
531
532        let mut nonrostered_round2 = BTreeMap::new();
533        nonrostered_round2.insert(14, Vec::new());
534        assert!(deserialize_round2_packages(&config, participant, nonrostered_round2).is_err());
535
536        let mut self_round2 = BTreeMap::new();
537        self_round2.insert(1, Vec::new());
538        assert!(deserialize_round2_packages(&config, participant, self_round2).is_err());
539
540        let mut malformed_round2 = BTreeMap::new();
541        malformed_round2.insert(2, b"not a round-two package".to_vec());
542        assert!(deserialize_round2_packages(&config, participant, malformed_round2).is_err());
543    }
544
545    #[test]
546    fn serialize_public_key_package_rejects_missing_verification_share() {
547        let config = test_config();
548        let mut rng = StdRng::seed_from_u64(7);
549        let dkg = run_complete_dkg(&config, &mut rng).expect("dkg");
550        let public: frost::keys::PublicKeyPackage =
551            deserialize_frost(dkg.public_key_package.public_key_package.as_slice())
552                .expect("public package");
553        let mut changed_config = config;
554        changed_config.certifiers[0].frost_identifier = 14;
555        assert!(serialize_public_key_package(&changed_config, &public).is_err());
556    }
557}