1use 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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
12pub struct RootPublicKeyPackage {
13 pub public_key_package: Vec<u8>,
15 pub root_public_key: Vec<u8>,
17 pub verifying_shares: BTreeMap<u16, Vec<u8>>,
19}
20
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
23pub struct RootKeyPackage {
24 pub frost_identifier: u16,
26 pub key_package: Vec<u8>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct RootDkgOutput {
33 pub key_packages: BTreeMap<u16, RootKeyPackage>,
35 pub public_key_package: RootPublicKeyPackage,
37}
38
39#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct RootDkgRound1Output {
42 pub frost_identifier: u16,
44 pub round1_secret_package: Vec<u8>,
46 pub round1_package: Vec<u8>,
48}
49
50#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub struct RootDkgRound2Output {
53 pub frost_identifier: u16,
55 pub round2_secret_package: Vec<u8>,
57 pub round2_packages: BTreeMap<u16, Vec<u8>>,
59}
60
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
63pub struct RootParticipantDkgOutput {
64 pub key_package: RootKeyPackage,
66 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
193pub 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
216pub 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
249pub 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
337pub 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}