Skip to main content

exo_root/
dkg.rs

1//! FROST DKG wrappers for root genesis.
2
3use std::{
4    collections::BTreeMap,
5    fmt::{self, Display},
6    io::Write,
7};
8
9use frost_ristretto255 as frost;
10use serde::{
11    Deserialize, Serialize,
12    de::{DeserializeOwned, MapAccess, SeqAccess, Visitor},
13};
14use zeroize::{Zeroize, Zeroizing};
15
16use crate::{GenesisCeremonyConfig, Result, RootError};
17
18/// Serialized public key package and derived public metadata.
19#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
20pub struct RootPublicKeyPackage {
21    /// Serialized FROST public key package.
22    pub public_key_package: Vec<u8>,
23    /// Serialized root verifying key.
24    pub root_public_key: Vec<u8>,
25    /// Serialized verification shares by FROST identifier.
26    pub verifying_shares: BTreeMap<u16, Vec<u8>>,
27}
28
29/// Serialized FROST key package held by one certifier.
30///
31/// The byte field retains its original [`Vec`] shape and caller-owned move
32/// semantics for patch-release source compatibility. Debug output is redacted;
33/// callers that retain this legacy carrier can invoke [`Zeroize::zeroize`]
34/// explicitly when the bytes are no longer needed.
35#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
36#[serde(from = "ZeroizingRootKeyPackage")]
37pub struct RootKeyPackage {
38    /// Owner's FROST identifier.
39    pub frost_identifier: u16,
40    /// Serialized FROST key package.
41    pub key_package: Vec<u8>,
42}
43
44impl fmt::Debug for RootKeyPackage {
45    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
46        formatter
47            .debug_struct("RootKeyPackage")
48            .field("frost_identifier", &self.frost_identifier)
49            .field("key_package", &"[REDACTED]")
50            .finish()
51    }
52}
53
54impl RootKeyPackage {
55    fn from_zeroizing(frost_identifier: u16, mut key_package: Zeroizing<Vec<u8>>) -> Self {
56        let mut output = Self {
57            frost_identifier,
58            key_package: Vec::new(),
59        };
60        output.key_package = std::mem::take(&mut *key_package);
61        output
62    }
63}
64
65impl Zeroize for RootKeyPackage {
66    fn zeroize(&mut self) {
67        self.key_package.zeroize();
68    }
69}
70
71#[derive(Deserialize)]
72struct ZeroizingRootKeyPackage {
73    frost_identifier: u16,
74    #[serde(deserialize_with = "deserialize_zeroizing_bytes")]
75    key_package: Zeroizing<Vec<u8>>,
76}
77
78impl From<ZeroizingRootKeyPackage> for RootKeyPackage {
79    fn from(value: ZeroizingRootKeyPackage) -> Self {
80        Self::from_zeroizing(value.frost_identifier, value.key_package)
81    }
82}
83
84/// Complete in-memory DKG result for tests and offline ceremony tooling.
85///
86/// Cloning this value duplicates private key-package material. Callers should
87/// keep copies short-lived and explicitly zeroize legacy private byte carriers
88/// when they are no longer needed.
89#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
90#[serde(from = "ZeroizingRootDkgOutput")]
91pub struct RootDkgOutput {
92    /// Certifier key packages by FROST identifier.
93    pub key_packages: BTreeMap<u16, RootKeyPackage>,
94    /// Public key package common to all certifiers.
95    pub public_key_package: RootPublicKeyPackage,
96}
97
98// Guard every decoded map value until all entries and public metadata parse.
99#[derive(Deserialize)]
100struct ZeroizingRootDkgOutput {
101    key_packages: BTreeMap<u16, ZeroizingRootKeyPackage>,
102    public_key_package: RootPublicKeyPackage,
103}
104
105impl From<ZeroizingRootDkgOutput> for RootDkgOutput {
106    fn from(value: ZeroizingRootDkgOutput) -> Self {
107        Self {
108            key_packages: value
109                .key_packages
110                .into_iter()
111                .map(|(identifier, package)| (identifier, package.into()))
112                .collect(),
113            public_key_package: value.public_key_package,
114        }
115    }
116}
117
118/// Serialized output from one certifier's DKG round one.
119///
120/// The fields retain their original [`Vec`] shapes and caller-owned move
121/// semantics for patch-release source compatibility. Debug output redacts the
122/// private round-one bytes, which can also be explicitly zeroized in place.
123#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(from = "ZeroizingRootDkgRound1Output")]
125pub struct RootDkgRound1Output {
126    /// Owner's FROST identifier.
127    pub frost_identifier: u16,
128    /// Private round-one state retained by the certifier.
129    pub round1_secret_package: Vec<u8>,
130    /// Public round-one package broadcast to every other certifier.
131    pub round1_package: Vec<u8>,
132}
133
134impl RootDkgRound1Output {
135    fn from_zeroizing(
136        frost_identifier: u16,
137        mut round1_secret_package: Zeroizing<Vec<u8>>,
138        round1_package: Vec<u8>,
139    ) -> Self {
140        let mut output = Self {
141            frost_identifier,
142            round1_secret_package: Vec::new(),
143            round1_package,
144        };
145        output.round1_secret_package = std::mem::take(&mut *round1_secret_package);
146        output
147    }
148}
149
150impl Zeroize for RootDkgRound1Output {
151    fn zeroize(&mut self) {
152        self.round1_secret_package.zeroize();
153    }
154}
155
156#[derive(Deserialize)]
157struct ZeroizingRootDkgRound1Output {
158    frost_identifier: u16,
159    #[serde(deserialize_with = "deserialize_zeroizing_bytes")]
160    round1_secret_package: Zeroizing<Vec<u8>>,
161    round1_package: Vec<u8>,
162}
163
164impl From<ZeroizingRootDkgRound1Output> for RootDkgRound1Output {
165    fn from(value: ZeroizingRootDkgRound1Output) -> Self {
166        Self::from_zeroizing(
167            value.frost_identifier,
168            value.round1_secret_package,
169            value.round1_package,
170        )
171    }
172}
173
174impl fmt::Debug for RootDkgRound1Output {
175    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
176        formatter
177            .debug_struct("RootDkgRound1Output")
178            .field("frost_identifier", &self.frost_identifier)
179            .field("round1_secret_package", &"[REDACTED]")
180            .field("round1_package", &self.round1_package)
181            .finish()
182    }
183}
184
185/// Serialized output from one certifier's DKG round two.
186///
187/// The fields retain their original [`Vec`] and [`BTreeMap`] shapes and
188/// caller-owned move semantics for patch-release source compatibility. Debug
189/// output redacts both private fields, which can also be explicitly zeroized in
190/// place.
191#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
192#[serde(from = "ZeroizingRootDkgRound2Output")]
193pub struct RootDkgRound2Output {
194    /// Owner's FROST identifier.
195    pub frost_identifier: u16,
196    /// Private round-two state retained by the certifier.
197    pub round2_secret_package: Vec<u8>,
198    /// Recipient-bound round-two packages by recipient FROST identifier.
199    pub round2_packages: BTreeMap<u16, Vec<u8>>,
200}
201
202type RecipientRound2Packages = BTreeMap<u16, Zeroizing<Vec<u8>>>;
203type ZeroizingRound2Parts = (Zeroizing<Vec<u8>>, RecipientRound2Packages);
204
205impl RootDkgRound2Output {
206    fn from_zeroizing(
207        frost_identifier: u16,
208        mut round2_secret_package: Zeroizing<Vec<u8>>,
209        mut round2_packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
210    ) -> Self {
211        let mut output = Self {
212            frost_identifier,
213            round2_secret_package: Vec::new(),
214            round2_packages: BTreeMap::new(),
215        };
216        output.round2_secret_package = std::mem::take(&mut *round2_secret_package);
217        while let Some((identifier, mut package)) = round2_packages.pop_first() {
218            output
219                .round2_packages
220                .insert(identifier, std::mem::take(&mut *package));
221        }
222        output
223    }
224
225    fn take_zeroizing_parts(&mut self) -> ZeroizingRound2Parts {
226        let secret_package = Zeroizing::new(std::mem::take(&mut self.round2_secret_package));
227        let packages = zeroizing_byte_map(std::mem::take(&mut self.round2_packages));
228        (secret_package, packages)
229    }
230}
231
232impl Zeroize for RootDkgRound2Output {
233    fn zeroize(&mut self) {
234        self.round2_secret_package.zeroize();
235        for package in self.round2_packages.values_mut() {
236            package.zeroize();
237        }
238        self.round2_packages.clear();
239    }
240}
241
242#[derive(Deserialize)]
243struct ZeroizingRootDkgRound2Output {
244    frost_identifier: u16,
245    #[serde(deserialize_with = "deserialize_zeroizing_bytes")]
246    round2_secret_package: Zeroizing<Vec<u8>>,
247    #[serde(deserialize_with = "deserialize_zeroizing_byte_map")]
248    round2_packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
249}
250
251impl From<ZeroizingRootDkgRound2Output> for RootDkgRound2Output {
252    fn from(value: ZeroizingRootDkgRound2Output) -> Self {
253        Self::from_zeroizing(
254            value.frost_identifier,
255            value.round2_secret_package,
256            value.round2_packages,
257        )
258    }
259}
260
261impl fmt::Debug for RootDkgRound2Output {
262    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
263        formatter
264            .debug_struct("RootDkgRound2Output")
265            .field("frost_identifier", &self.frost_identifier)
266            .field("round2_secret_package", &"[REDACTED]")
267            .field("round2_packages", &"[REDACTED]")
268            .finish()
269    }
270}
271
272const MAX_INITIAL_SECRET_CAPACITY: usize = 4096;
273const MAX_SECRET_CAPACITY: usize = usize::MAX / 2;
274
275struct ZeroizingByteAccumulator {
276    bytes: Zeroizing<Vec<u8>>,
277    logical_len: usize,
278}
279
280#[derive(Clone, Copy, Debug)]
281struct SecretBufferError(&'static str);
282
283fn next_secret_capacity(current_capacity: usize) -> std::result::Result<usize, SecretBufferError> {
284    let next_capacity = current_capacity
285        .checked_mul(2)
286        .unwrap_or(MAX_SECRET_CAPACITY)
287        .clamp(1, MAX_SECRET_CAPACITY);
288    if next_capacity <= current_capacity {
289        return Err(SecretBufferError(
290            "root secret bytes exceed supported capacity",
291        ));
292    }
293    Ok(next_capacity)
294}
295
296impl ZeroizingByteAccumulator {
297    fn with_size_hint(size_hint: usize) -> std::result::Result<Self, SecretBufferError> {
298        let initial_capacity = size_hint.min(MAX_INITIAL_SECRET_CAPACITY);
299        let mut bytes = Zeroizing::new(Vec::new());
300        bytes
301            .try_reserve_exact(initial_capacity)
302            .map_err(|_| SecretBufferError("unable to allocate root secret bytes"))?;
303        bytes.resize(initial_capacity, 0);
304        Ok(Self {
305            bytes,
306            logical_len: 0,
307        })
308    }
309
310    fn push(&mut self, byte: u8) -> std::result::Result<(), SecretBufferError> {
311        if self.logical_len == self.bytes.len() {
312            self.grow()?;
313        }
314        self.bytes[self.logical_len] = byte;
315        self.logical_len += 1;
316        Ok(())
317    }
318
319    fn grow(&mut self) -> std::result::Result<(), SecretBufferError> {
320        let current_capacity = self.bytes.len();
321        let next_capacity = next_secret_capacity(current_capacity)?;
322        let mut replacement = Zeroizing::new(Vec::new());
323        replacement
324            .try_reserve_exact(next_capacity)
325            .map_err(|_| SecretBufferError("unable to allocate root secret bytes"))?;
326        replacement.resize(next_capacity, 0);
327        replacement[..self.logical_len].copy_from_slice(&self.bytes[..self.logical_len]);
328        self.bytes.zeroize();
329        self.bytes = replacement;
330        Ok(())
331    }
332
333    fn finish(mut self) -> Zeroizing<Vec<u8>> {
334        self.bytes.truncate(self.logical_len);
335        std::mem::take(&mut self.bytes)
336    }
337}
338
339impl Drop for ZeroizingByteAccumulator {
340    fn drop(&mut self) {
341        self.bytes.zeroize();
342        self.logical_len.zeroize();
343    }
344}
345
346impl zeroize::ZeroizeOnDrop for ZeroizingByteAccumulator {}
347
348struct ZeroizingByteWriter {
349    accumulator: ZeroizingByteAccumulator,
350}
351
352impl ZeroizingByteWriter {
353    fn new() -> Self {
354        Self {
355            accumulator: ZeroizingByteAccumulator {
356                bytes: Zeroizing::new(Vec::new()),
357                logical_len: 0,
358            },
359        }
360    }
361
362    fn finish(self) -> Zeroizing<Vec<u8>> {
363        self.accumulator.finish()
364    }
365}
366
367impl Write for ZeroizingByteWriter {
368    fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
369        for byte in buffer {
370            self.accumulator
371                .push(*byte)
372                .map_err(|error| std::io::Error::new(std::io::ErrorKind::OutOfMemory, error.0))?;
373        }
374        Ok(buffer.len())
375    }
376
377    fn flush(&mut self) -> std::io::Result<()> {
378        Ok(())
379    }
380}
381
382impl zeroize::ZeroizeOnDrop for ZeroizingByteWriter {}
383
384struct ZeroizingBytesVisitor;
385
386impl<'de> Visitor<'de> for ZeroizingBytesVisitor {
387    type Value = Zeroizing<Vec<u8>>;
388
389    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
390        formatter.write_str("a sequence of root secret bytes")
391    }
392
393    fn visit_seq<A>(self, mut sequence: A) -> std::result::Result<Self::Value, A::Error>
394    where
395        A: SeqAccess<'de>,
396    {
397        let mut accumulator =
398            ZeroizingByteAccumulator::with_size_hint(sequence.size_hint().unwrap_or(0))
399                .map_err(|error| <A::Error as serde::de::Error>::custom(error.0))?;
400        while let Some(byte) = sequence.next_element::<u8>()? {
401            accumulator
402                .push(byte)
403                .map_err(|error| <A::Error as serde::de::Error>::custom(error.0))?;
404        }
405        Ok(accumulator.finish())
406    }
407}
408
409pub(crate) fn deserialize_zeroizing_bytes<'de, D>(
410    deserializer: D,
411) -> std::result::Result<Zeroizing<Vec<u8>>, D::Error>
412where
413    D: serde::Deserializer<'de>,
414{
415    deserializer.deserialize_seq(ZeroizingBytesVisitor)
416}
417
418struct ZeroizingBytes(Zeroizing<Vec<u8>>);
419
420impl<'de> Deserialize<'de> for ZeroizingBytes {
421    fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
422    where
423        D: serde::Deserializer<'de>,
424    {
425        deserialize_zeroizing_bytes(deserializer).map(Self)
426    }
427}
428
429struct ZeroizingByteMapVisitor;
430
431impl<'de> Visitor<'de> for ZeroizingByteMapVisitor {
432    type Value = BTreeMap<u16, Zeroizing<Vec<u8>>>;
433
434    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
435        formatter.write_str("a map of recipient-bound root secret byte sequences")
436    }
437
438    fn visit_map<A>(self, mut map: A) -> std::result::Result<Self::Value, A::Error>
439    where
440        A: MapAccess<'de>,
441    {
442        let mut packages = BTreeMap::new();
443        while let Some((identifier, package)) = map.next_entry::<u16, ZeroizingBytes>()? {
444            packages.insert(identifier, package.0);
445        }
446        Ok(packages)
447    }
448}
449
450fn deserialize_zeroizing_byte_map<'de, D>(
451    deserializer: D,
452) -> std::result::Result<BTreeMap<u16, Zeroizing<Vec<u8>>>, D::Error>
453where
454    D: serde::Deserializer<'de>,
455{
456    deserializer.deserialize_map(ZeroizingByteMapVisitor)
457}
458
459fn zeroizing_byte_map(mut packages: BTreeMap<u16, Vec<u8>>) -> BTreeMap<u16, Zeroizing<Vec<u8>>> {
460    let mut zeroizing_packages = BTreeMap::new();
461    while let Some((identifier, package)) = packages.pop_first() {
462        let package = Zeroizing::new(package);
463        zeroizing_packages.insert(identifier, package);
464    }
465    zeroizing_packages
466}
467
468/// Final DKG material derived by one certifier.
469///
470/// Cloning this value duplicates its private key package. Callers should keep
471/// copies short-lived and explicitly zeroize legacy private byte carriers when
472/// they are no longer needed.
473#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
474#[serde(from = "ZeroizingRootParticipantDkgOutput")]
475pub struct RootParticipantDkgOutput {
476    /// Owner's FROST key package.
477    pub key_package: RootKeyPackage,
478    /// Public root key package derived by the participant.
479    pub public_key_package: RootPublicKeyPackage,
480}
481
482impl Zeroize for RootParticipantDkgOutput {
483    fn zeroize(&mut self) {
484        self.key_package.zeroize();
485    }
486}
487
488#[derive(Deserialize)]
489struct ZeroizingRootParticipantDkgOutput {
490    key_package: ZeroizingRootKeyPackage,
491    public_key_package: RootPublicKeyPackage,
492}
493
494impl From<ZeroizingRootParticipantDkgOutput> for RootParticipantDkgOutput {
495    fn from(value: ZeroizingRootParticipantDkgOutput) -> Self {
496        Self {
497            key_package: value.key_package.into(),
498            public_key_package: value.public_key_package,
499        }
500    }
501}
502
503pub(crate) fn frost_identifier(identifier: u16) -> Result<frost::Identifier> {
504    frost::Identifier::try_from(identifier).map_err(frost_error)
505}
506
507fn rostered_frost_identifier(
508    config: &GenesisCeremonyConfig,
509    identifier: u16,
510    operation: &str,
511) -> Result<frost::Identifier> {
512    if config.certifier_by_identifier(identifier).is_none() {
513        return Err(RootError::InvalidConfig {
514            reason: format!("{operation} certifier {identifier} is not rostered"),
515        });
516    }
517    frost_identifier(identifier)
518}
519
520fn frost_error(error: frost::Error) -> RootError {
521    RootError::Frost {
522        detail: error.to_string(),
523    }
524}
525
526fn frost_encoding_error(error: impl Display) -> RootError {
527    RootError::Frost {
528        detail: format!("FROST artifact canonical encoding failed: {error}"),
529    }
530}
531
532pub(crate) fn serialize_frost<T: Serialize>(value: &T) -> Result<Vec<u8>> {
533    let mut bytes = Vec::new();
534    ciborium::into_writer(value, &mut bytes).map_err(frost_encoding_error)?;
535    Ok(bytes)
536}
537
538pub(crate) fn serialize_frost_secret<T: Serialize>(value: &T) -> Result<Zeroizing<Vec<u8>>> {
539    let mut writer = ZeroizingByteWriter::new();
540    ciborium::into_writer(value, &mut writer).map_err(frost_encoding_error)?;
541    Ok(writer.finish())
542}
543
544pub(crate) fn deserialize_frost<T: DeserializeOwned>(bytes: &[u8]) -> Result<T> {
545    ciborium::from_reader(bytes).map_err(frost_encoding_error)
546}
547
548fn identifier_value(
549    config: &GenesisCeremonyConfig,
550    frost_identifier: frost::Identifier,
551) -> Result<u16> {
552    for certifier in &config.certifiers {
553        if crate::dkg::frost_identifier(certifier.frost_identifier)? == frost_identifier {
554            return Ok(certifier.frost_identifier);
555        }
556    }
557    Err(RootError::Frost {
558        detail: "FROST identifier is not rostered".to_owned(),
559    })
560}
561
562fn peer_packages_except(
563    packages: &BTreeMap<u16, Vec<u8>>,
564    excluded: u16,
565) -> BTreeMap<u16, Vec<u8>> {
566    packages
567        .iter()
568        .filter(|(peer, _)| **peer != excluded)
569        .map(|(peer, package)| (*peer, package.clone()))
570        .collect()
571}
572
573fn frost_dkg_round1<R>(
574    identifier: frost::Identifier,
575    max_signers: u16,
576    threshold: u16,
577    rng: &mut R,
578) -> Result<(
579    frost::keys::dkg::round1::SecretPackage,
580    frost::keys::dkg::round1::Package,
581)>
582where
583    R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
584{
585    frost::keys::dkg::part1(identifier, max_signers, threshold, rng).map_err(frost_error)
586}
587
588pub(crate) fn serialize_public_key_package(
589    config: &GenesisCeremonyConfig,
590    package: &frost::keys::PublicKeyPackage,
591) -> Result<RootPublicKeyPackage> {
592    let public_key_package = serialize_frost(package)?;
593    let root_public_key = serialize_frost(package.verifying_key())?;
594    let mut verifying_shares = BTreeMap::new();
595    for certifier in &config.certifiers {
596        let identifier = frost_identifier(certifier.frost_identifier)?;
597        let share =
598            package
599                .verifying_shares()
600                .get(&identifier)
601                .ok_or_else(|| RootError::Frost {
602                    detail: format!(
603                        "missing verification share for identifier {}",
604                        certifier.frost_identifier
605                    ),
606                })?;
607        verifying_shares.insert(certifier.frost_identifier, serialize_frost(share)?);
608    }
609    Ok(RootPublicKeyPackage {
610        public_key_package,
611        root_public_key,
612        verifying_shares,
613    })
614}
615
616pub(crate) fn validate_public_key_package(
617    config: &GenesisCeremonyConfig,
618    package: &RootPublicKeyPackage,
619) -> Result<()> {
620    let frost_package: frost::keys::PublicKeyPackage =
621        deserialize_frost(package.public_key_package.as_slice())?;
622    let expected = serialize_public_key_package(config, &frost_package)?;
623    if &expected != package {
624        return Err(RootError::BundleRejected {
625            reason: "public key package metadata does not match serialized FROST package"
626                .to_owned(),
627        });
628    }
629    Ok(())
630}
631
632/// Execute DKG round one for one rostered certifier.
633pub fn dkg_round1<R>(
634    config: &GenesisCeremonyConfig,
635    frost_identifier_value: u16,
636    rng: &mut R,
637) -> Result<RootDkgRound1Output>
638where
639    R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
640{
641    config.validate()?;
642    let identifier = rostered_frost_identifier(config, frost_identifier_value, "round-one")?;
643    let max_signers = config.max_signers;
644    let threshold = config.threshold;
645    let round1 = frost_dkg_round1(identifier, max_signers, threshold, rng)?;
646    let (secret_package, package) = round1;
647    let secret_package = serialize_frost_secret(&secret_package)?;
648    let public_package = serialize_frost(&package)?;
649    Ok(RootDkgRound1Output::from_zeroizing(
650        frost_identifier_value,
651        secret_package,
652        public_package,
653    ))
654}
655
656/// Execute DKG round two for one certifier after all other round-one packages
657/// have been authenticated and collected.
658pub fn dkg_round2(
659    config: &GenesisCeremonyConfig,
660    frost_identifier_value: u16,
661    round1_secret_package: &[u8],
662    round1_packages: BTreeMap<u16, Vec<u8>>,
663) -> Result<RootDkgRound2Output> {
664    config.validate()?;
665    if round1_packages.len() != usize::from(config.max_signers - 1) {
666        return Err(RootError::Frost {
667            detail: "round two requires all twelve peer round-one packages".to_owned(),
668        });
669    }
670    let participant_identifier =
671        rostered_frost_identifier(config, frost_identifier_value, "round-two")?;
672    let secret_package = deserialize_frost(round1_secret_package)?;
673    let inbound_round1 =
674        deserialize_round1_packages(config, participant_identifier, round1_packages)?;
675    let (round2_secret_package, outbound) =
676        frost::keys::dkg::part2(secret_package, &inbound_round1).map_err(frost_error)?;
677    let mut round2_packages = BTreeMap::new();
678    for (recipient, package) in outbound {
679        let recipient_value = identifier_value(config, recipient)?;
680        round2_packages.insert(recipient_value, serialize_frost_secret(&package)?);
681    }
682    let round2_secret_package = serialize_frost_secret(&round2_secret_package)?;
683    Ok(RootDkgRound2Output::from_zeroizing(
684        frost_identifier_value,
685        round2_secret_package,
686        round2_packages,
687    ))
688}
689
690/// Finalize one participant's DKG state after all peer round-one and round-two
691/// packages have been authenticated and collected.
692pub fn dkg_finalize_participant(
693    config: &GenesisCeremonyConfig,
694    frost_identifier_value: u16,
695    round2_secret_package: &[u8],
696    round1_packages: BTreeMap<u16, Vec<u8>>,
697    round2_packages: BTreeMap<u16, Vec<u8>>,
698) -> Result<RootParticipantDkgOutput> {
699    let round2_packages = zeroizing_byte_map(round2_packages);
700    dkg_finalize_participant_zeroizing(
701        config,
702        frost_identifier_value,
703        round2_secret_package,
704        round1_packages,
705        round2_packages,
706    )
707}
708
709/// Finalize one participant while retaining zeroizing ownership of decoded
710/// recipient-bound round-two packages.
711///
712/// Ceremony tooling that already decodes private packages into [`Zeroizing`]
713/// buffers should use this entry point. Existing callers with ordinary byte
714/// vectors remain supported by [`dkg_finalize_participant`].
715pub fn dkg_finalize_participant_zeroizing(
716    config: &GenesisCeremonyConfig,
717    frost_identifier_value: u16,
718    round2_secret_package: &[u8],
719    round1_packages: BTreeMap<u16, Vec<u8>>,
720    round2_packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
721) -> Result<RootParticipantDkgOutput> {
722    config.validate()?;
723    if round1_packages.len() != usize::from(config.max_signers - 1) {
724        return Err(RootError::Frost {
725            detail: "finalize requires all twelve peer round-one packages".to_owned(),
726        });
727    }
728    if round2_packages.len() != usize::from(config.max_signers - 1) {
729        return Err(RootError::Frost {
730            detail: "finalize requires all twelve peer round-two packages".to_owned(),
731        });
732    }
733    let participant_identifier =
734        rostered_frost_identifier(config, frost_identifier_value, "finalize")?;
735    let secret_package = deserialize_frost(round2_secret_package)?;
736    let inbound_round1 =
737        deserialize_round1_packages(config, participant_identifier, round1_packages)?;
738    let inbound_round2 =
739        deserialize_round2_packages(config, participant_identifier, round2_packages)?;
740    let (key_package, public_key_package) =
741        frost::keys::dkg::part3(&secret_package, &inbound_round1, &inbound_round2)
742            .map_err(frost_error)?;
743    let key_package = serialize_frost_secret(&key_package)?;
744    let public_key_package = serialize_public_key_package(config, &public_key_package)?;
745    Ok(RootParticipantDkgOutput {
746        key_package: RootKeyPackage::from_zeroizing(frost_identifier_value, key_package),
747        public_key_package,
748    })
749}
750
751fn deserialize_round1_packages(
752    config: &GenesisCeremonyConfig,
753    participant_identifier: frost::Identifier,
754    packages: BTreeMap<u16, Vec<u8>>,
755) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round1::Package>> {
756    let mut result = BTreeMap::new();
757    for (sender, package_bytes) in packages {
758        if config.certifier_by_identifier(sender).is_none() {
759            return Err(RootError::InvalidConfig {
760                reason: format!("round-one sender {sender} is not rostered"),
761            });
762        }
763        let sender_identifier = frost_identifier(sender)?;
764        if sender_identifier == participant_identifier {
765            return Err(RootError::Frost {
766                detail: "round-one peer packages must not include self".to_owned(),
767            });
768        }
769        let package = deserialize_frost(package_bytes.as_slice())?;
770        result.insert(sender_identifier, package);
771    }
772    Ok(result)
773}
774
775fn deserialize_round2_packages(
776    config: &GenesisCeremonyConfig,
777    participant_identifier: frost::Identifier,
778    packages: BTreeMap<u16, Zeroizing<Vec<u8>>>,
779) -> Result<BTreeMap<frost::Identifier, frost::keys::dkg::round2::Package>> {
780    let mut result = BTreeMap::new();
781    for (sender, package_bytes) in packages {
782        if config.certifier_by_identifier(sender).is_none() {
783            return Err(RootError::InvalidConfig {
784                reason: format!("round-two sender {sender} is not rostered"),
785            });
786        }
787        let sender_identifier = frost_identifier(sender)?;
788        if sender_identifier == participant_identifier {
789            return Err(RootError::Frost {
790                detail: "round-two peer packages must not include self".to_owned(),
791            });
792        }
793        let package = deserialize_frost(package_bytes.as_slice())?;
794        result.insert(sender_identifier, package);
795    }
796    Ok(result)
797}
798
799fn require_recipient_round2_packages(
800    round2_packages: Option<RecipientRound2Packages>,
801    identifier: u16,
802) -> Result<RecipientRound2Packages> {
803    round2_packages.ok_or_else(|| RootError::Frost {
804        detail: format!("missing recipient-bound round-two packages for {identifier}"),
805    })
806}
807
808fn require_first_public_key_package(
809    public_key_package: Option<RootPublicKeyPackage>,
810) -> Result<RootPublicKeyPackage> {
811    public_key_package.ok_or_else(|| RootError::Frost {
812        detail: "missing first participant public key package".to_owned(),
813    })
814}
815
816/// Run the all-roster DKG ceremony locally.
817///
818/// Production ceremonies should exchange these packages through the portal and
819/// pairwise encrypted channels. This function enforces the same all-thirteen
820/// completion rule for deterministic regression tests and offline rehearsals.
821pub fn run_complete_dkg<R>(config: &GenesisCeremonyConfig, rng: &mut R) -> Result<RootDkgOutput>
822where
823    R: frost::rand_core::RngCore + frost::rand_core::CryptoRng,
824{
825    config.validate()?;
826
827    let mut round1_outputs: BTreeMap<u16, Zeroizing<RootDkgRound1Output>> = BTreeMap::new();
828    let mut round1_public = BTreeMap::new();
829    for certifier in &config.certifiers {
830        let output = Zeroizing::new(dkg_round1(config, certifier.frost_identifier, rng)?);
831        round1_public.insert(certifier.frost_identifier, output.round1_package.clone());
832        round1_outputs.insert(certifier.frost_identifier, output);
833    }
834
835    let mut round2_secrets = BTreeMap::new();
836    let mut round2_by_recipient: BTreeMap<u16, RecipientRound2Packages> = BTreeMap::new();
837    for (identifier, round1_output) in round1_outputs {
838        let peer_round1 = peer_packages_except(&round1_public, identifier);
839        let secret = &round1_output.round1_secret_package;
840        let mut round2 = dkg_round2(config, identifier, secret, peer_round1)?;
841        let (round2_secret, round2_packages) = round2.take_zeroizing_parts();
842        for (recipient, package) in round2_packages {
843            let recipient_packages = round2_by_recipient.entry(recipient).or_default();
844            recipient_packages.insert(identifier, package);
845        }
846        round2_secrets.insert(identifier, round2_secret);
847    }
848
849    let mut key_packages: BTreeMap<u16, Zeroizing<RootKeyPackage>> = BTreeMap::new();
850    let first_identifier = config.certifiers[0].frost_identifier;
851    let mut public_key_package = None;
852    for (identifier, round2_secret) in round2_secrets {
853        let peer_round1 = peer_packages_except(&round1_public, identifier);
854        let recipient_packages = round2_by_recipient.remove(&identifier);
855        let round2 = require_recipient_round2_packages(recipient_packages, identifier)?;
856        let participant = dkg_finalize_participant_zeroizing(
857            config,
858            identifier,
859            &round2_secret,
860            peer_round1,
861            round2,
862        )?;
863        let key_package = Zeroizing::new(participant.key_package);
864        if identifier == first_identifier {
865            public_key_package = Some(participant.public_key_package);
866        }
867        key_packages.insert(identifier, key_package);
868    }
869
870    let public_key_package = require_first_public_key_package(public_key_package)?;
871    // Only a successful public handoff releases legacy caller-owned vectors.
872    let key_packages = key_packages
873        .into_iter()
874        .map(|(identifier, mut package)| {
875            let key_package = RootKeyPackage {
876                frost_identifier: package.frost_identifier,
877                key_package: std::mem::take(&mut package.key_package),
878            };
879            (identifier, key_package)
880        })
881        .collect();
882    Ok(RootDkgOutput {
883        key_packages,
884        public_key_package,
885    })
886}
887
888#[cfg(test)]
889mod tests {
890    use exo_core::{Did, Hash256, PublicKey, Timestamp};
891    use rand::{SeedableRng, rngs::StdRng};
892    use serde::{Serialize, ser::SerializeSeq};
893
894    use super::*;
895    use crate::CertifierContact;
896
897    fn test_config() -> GenesisCeremonyConfig {
898        let certifiers = (1..=13)
899            .map(|identifier| {
900                let byte = u8::try_from(identifier).expect("identifier fits");
901                CertifierContact {
902                    did: Did::new(&format!("did:exo:unit-{identifier:02}")).expect("valid did"),
903                    frost_identifier: identifier,
904                    signing_public_key: PublicKey::from_bytes([byte; 32]),
905                    transport_public_key: [byte; 32],
906                }
907            })
908            .collect();
909        GenesisCeremonyConfig {
910            ceremony_id: "unit-root".into(),
911            network_id: "unit-net".into(),
912            repo_commit: "d8927686a34bdc28ba36d53938f665685d2c4c04".into(),
913            constitution_hash: Hash256::digest(b"constitution"),
914            threshold: 7,
915            max_signers: 13,
916            created_at: Timestamp::new(1, 0),
917            certifiers,
918            signing_set: (1..=7).collect(),
919        }
920    }
921
922    #[derive(Serialize)]
923    struct LegacyRootKeyPackage<'a> {
924        frost_identifier: u16,
925        key_package: &'a Vec<u8>,
926    }
927
928    #[derive(Serialize)]
929    struct LegacyRound1Output<'a> {
930        frost_identifier: u16,
931        round1_secret_package: &'a Vec<u8>,
932        round1_package: &'a Vec<u8>,
933    }
934
935    #[derive(Serialize)]
936    struct LegacyRound2Output<'a> {
937        frost_identifier: u16,
938        round2_secret_package: &'a Vec<u8>,
939        round2_packages: &'a BTreeMap<u16, Vec<u8>>,
940    }
941
942    #[derive(Serialize)]
943    struct MalformedKeyPackage {
944        frost_identifier: u16,
945        key_package: Vec<serde_json::Value>,
946    }
947
948    #[derive(Serialize)]
949    struct MalformedRound1Output {
950        frost_identifier: u16,
951        round1_secret_package: Vec<serde_json::Value>,
952        round1_package: Vec<u8>,
953    }
954
955    #[derive(Serialize)]
956    struct MalformedRound2SecretOutput {
957        frost_identifier: u16,
958        round2_secret_package: Vec<serde_json::Value>,
959        round2_packages: BTreeMap<u16, Vec<u8>>,
960    }
961
962    #[derive(Serialize)]
963    struct MalformedRound2MapOutput {
964        frost_identifier: u16,
965        round2_secret_package: Vec<u8>,
966        round2_packages: BTreeMap<u16, Vec<serde_json::Value>>,
967    }
968
969    struct FailingSecretSerialize;
970
971    impl Serialize for FailingSecretSerialize {
972        fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
973        where
974            S: serde::Serializer,
975        {
976            let mut sequence = serializer.serialize_seq(Some(4))?;
977            sequence.serialize_element(&0xdeu8)?;
978            sequence.serialize_element(&0xadu8)?;
979            Err(<S::Error as serde::ser::Error>::custom(
980                "forced secret serialization failure",
981            ))
982        }
983    }
984
985    struct NoSizeHintByteSequence {
986        bytes: std::vec::IntoIter<u8>,
987    }
988
989    impl<'de> serde::de::SeqAccess<'de> for NoSizeHintByteSequence {
990        type Error = serde::de::value::Error;
991
992        fn next_element_seed<T>(
993            &mut self,
994            seed: T,
995        ) -> std::result::Result<Option<T::Value>, Self::Error>
996        where
997            T: serde::de::DeserializeSeed<'de>,
998        {
999            match self.bytes.next() {
1000                Some(byte) => seed
1001                    .deserialize(serde::de::value::U8Deserializer::new(byte))
1002                    .map(Some),
1003                None => Ok(None),
1004            }
1005        }
1006
1007        fn size_hint(&self) -> Option<usize> {
1008            None
1009        }
1010    }
1011
1012    fn cbor_bytes(value: &impl Serialize) -> Vec<u8> {
1013        let mut bytes = Vec::new();
1014        ciborium::into_writer(value, &mut bytes).expect("CBOR encoding");
1015        bytes
1016    }
1017
1018    fn assert_explicitly_zeroizable<T: zeroize::Zeroize>(_: &T) {}
1019
1020    #[test]
1021    fn legacy_secret_dkg_byte_carriers_support_explicit_zeroize() {
1022        let mut key_package = RootKeyPackage {
1023            frost_identifier: 7,
1024            key_package: vec![0xde, 0xad, 0xbe, 0xef],
1025        };
1026        let mut round1 = RootDkgRound1Output {
1027            frost_identifier: 7,
1028            round1_secret_package: vec![0xca, 0xfe, 0xba, 0xbe],
1029            round1_package: vec![1, 2, 3],
1030        };
1031        let mut round2 = RootDkgRound2Output {
1032            frost_identifier: 7,
1033            round2_secret_package: vec![0x12, 0x34, 0x56, 0x78],
1034            round2_packages: BTreeMap::from([(8, vec![4, 5, 6])]),
1035        };
1036
1037        assert_explicitly_zeroizable(&key_package);
1038        assert_explicitly_zeroizable(&round1);
1039        assert_explicitly_zeroizable(&round2);
1040
1041        key_package.zeroize();
1042        round1.zeroize();
1043        round2.zeroize();
1044        assert!(key_package.key_package.is_empty());
1045        assert!(round1.round1_secret_package.is_empty());
1046        assert!(round2.round2_secret_package.is_empty());
1047        assert!(round2.round2_packages.is_empty());
1048        let public_key_package = RootPublicKeyPackage {
1049            public_key_package: vec![1],
1050            root_public_key: vec![2],
1051            verifying_shares: BTreeMap::from([(7, vec![3])]),
1052        };
1053        let mut participant = RootParticipantDkgOutput {
1054            key_package: RootKeyPackage {
1055                frost_identifier: 7,
1056                key_package: vec![4, 5, 6],
1057            },
1058            public_key_package: public_key_package.clone(),
1059        };
1060        assert_explicitly_zeroizable(&participant);
1061        participant.zeroize();
1062        assert!(participant.key_package.key_package.is_empty());
1063        assert_eq!(participant.key_package.frost_identifier, 7);
1064        assert_eq!(participant.public_key_package, public_key_package);
1065    }
1066
1067    #[test]
1068    fn secret_dkg_debug_redacts_every_private_package() {
1069        let key_secret = vec![0xde, 0xad, 0xbe, 0xef];
1070        let round1_secret = vec![0xca, 0xfe, 0xba, 0xbe];
1071        let round2_secret = vec![0x12, 0x34, 0x56, 0x78];
1072        let key_package = RootKeyPackage {
1073            frost_identifier: 7,
1074            key_package: key_secret.clone(),
1075        };
1076        let round1 = RootDkgRound1Output {
1077            frost_identifier: 7,
1078            round1_secret_package: round1_secret.clone(),
1079            round1_package: vec![1, 2, 3],
1080        };
1081        let round2 = RootDkgRound2Output {
1082            frost_identifier: 7,
1083            round2_secret_package: round2_secret.clone(),
1084            round2_packages: BTreeMap::from([(8, vec![4, 5, 6])]),
1085        };
1086
1087        for (rendered, secret) in [
1088            (format!("{key_package:?}"), format!("{key_secret:?}")),
1089            (format!("{round1:?}"), format!("{round1_secret:?}")),
1090            (format!("{round2:?}"), format!("{round2_secret:?}")),
1091        ] {
1092            assert!(
1093                rendered.contains("[REDACTED]"),
1094                "secret-bearing DKG debug output must visibly redact: {rendered}"
1095            );
1096            assert!(
1097                !rendered.contains(&secret),
1098                "secret-bearing DKG debug output exposed fixture bytes: {rendered}"
1099            );
1100        }
1101    }
1102
1103    #[test]
1104    fn secret_round_two_recipient_packages_are_fully_redacted() {
1105        let recipient_secret = vec![0x99, 0x88, 0x77, 0x66];
1106        let output = RootDkgRound2Output {
1107            frost_identifier: 7,
1108            round2_secret_package: vec![0x12, 0x34, 0x56, 0x78],
1109            round2_packages: BTreeMap::from([(8, recipient_secret.clone())]),
1110        };
1111
1112        let rendered = format!("{output:?}");
1113        assert!(
1114            rendered.matches("[REDACTED]").count() >= 2,
1115            "both round-two secret fields must be visibly redacted: {rendered}"
1116        );
1117        assert!(
1118            !rendered.contains(&format!("{recipient_secret:?}")),
1119            "recipient-bound round-two package leaked through Debug: {rendered}"
1120        );
1121        assert_explicitly_zeroizable(&output);
1122    }
1123
1124    #[test]
1125    fn secret_byte_deserializer_clamps_hints_and_grows_from_fully_initialized_storage() {
1126        let mut accumulator = ZeroizingByteAccumulator::with_size_hint(usize::MAX)
1127            .expect("bounded initial allocation");
1128        assert_eq!(accumulator.bytes.len(), accumulator.bytes.capacity());
1129        assert!(accumulator.bytes.capacity() <= 4096);
1130
1131        let bytes_to_add = accumulator.bytes.capacity() + 1;
1132        for index in 0..bytes_to_add {
1133            accumulator
1134                .push(u8::try_from(index % 251).expect("fixture byte"))
1135                .expect("zeroizing growth");
1136        }
1137        assert_eq!(accumulator.bytes.len(), accumulator.bytes.capacity());
1138        let finished = accumulator.finish();
1139        assert_eq!(finished.len(), bytes_to_add);
1140        assert_explicitly_zeroizable(&finished);
1141    }
1142
1143    #[test]
1144    fn secret_byte_deserializer_accepts_sequences_without_size_hints() {
1145        let sequence = NoSizeHintByteSequence {
1146            bytes: vec![0xde, 0xad, 0xbe, 0xef].into_iter(),
1147        };
1148        let deserializer = serde::de::value::SeqAccessDeserializer::new(sequence);
1149
1150        let mut decoded =
1151            deserialize_zeroizing_bytes(deserializer).expect("sequence without a size hint");
1152        assert_eq!(decoded.as_slice(), &[0xde, 0xad, 0xbe, 0xef]);
1153        decoded.zeroize();
1154        assert!(decoded.is_empty());
1155    }
1156
1157    #[test]
1158    fn secret_byte_capacity_guard_rejects_terminal_capacity() {
1159        let error = next_secret_capacity(MAX_SECRET_CAPACITY)
1160            .expect_err("the bounded secret buffer must never grow past its terminal capacity");
1161        assert_eq!(
1162            error.0, "root secret bytes exceed supported capacity",
1163            "capacity exhaustion must remain a typed, diagnostic failure"
1164        );
1165    }
1166
1167    #[test]
1168    fn secret_byte_writer_wipes_before_growth_and_on_serialization_error() {
1169        fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>() {}
1170        assert_zeroize_on_drop::<ZeroizingByteAccumulator>();
1171        assert_zeroize_on_drop::<ZeroizingByteWriter>();
1172
1173        let fixture: Vec<u8> = (0..=u16::try_from(MAX_INITIAL_SECRET_CAPACITY).unwrap_or(u16::MAX))
1174            .map(|index| u8::try_from(index % 251).expect("fixture byte"))
1175            .collect();
1176        let mut writer = ZeroizingByteWriter::new();
1177        writer.write_all(&fixture).expect("forced-growth write");
1178        writer
1179            .flush()
1180            .expect("the unbuffered writer flushes safely");
1181        assert_eq!(
1182            writer.accumulator.bytes.len(),
1183            writer.accumulator.bytes.capacity()
1184        );
1185        assert_eq!(writer.finish().as_slice(), fixture.as_slice());
1186
1187        let error = serialize_frost_secret(&FailingSecretSerialize)
1188            .expect_err("serialization error must not release a secret buffer");
1189        assert!(
1190            error
1191                .to_string()
1192                .contains("forced secret serialization failure")
1193        );
1194    }
1195
1196    fn derive_before<'a>(source: &'a str, declaration: &str) -> &'a str {
1197        let declaration_offset = source
1198            .find(declaration)
1199            .expect("secret carrier declaration");
1200        let prefix = &source[..declaration_offset];
1201        let derive_offset = prefix.rfind("#[derive(").expect("secret carrier derive");
1202        &prefix[derive_offset..]
1203    }
1204
1205    #[test]
1206    fn complete_dkg_moves_internal_zeroizing_secret_packages_without_clone() {
1207        let dkg_source = include_str!("dkg.rs").split("#[cfg(test)]").next().unwrap();
1208        let signing_source = include_str!("signing.rs")
1209            .split("#[cfg(test)]")
1210            .next()
1211            .unwrap();
1212        for (declaration, intermediate) in [
1213            ("pub struct RootDkgOutput", "ZeroizingRootDkgOutput"),
1214            (
1215                "pub struct RootParticipantDkgOutput",
1216                "ZeroizingRootParticipantDkgOutput",
1217            ),
1218        ] {
1219            assert!(
1220                derive_before(dkg_source, declaration)
1221                    .contains(&format!("#[serde(from = \"{intermediate}\")]")),
1222                "{declaration} must guard private fields before later composite decoding fails"
1223            );
1224        }
1225        // These private deserializer fields retain drop guards even if a
1226        // subsequent map entry or public-metadata field fails to deserialize.
1227        assert!(dkg_source.contains("key_packages: BTreeMap<u16, ZeroizingRootKeyPackage>"));
1228        assert!(dkg_source.contains("key_package: ZeroizingRootKeyPackage"));
1229        let signing_production = signing_source.split("#[cfg(test)]").next().unwrap();
1230        let legacy_signing = signing_production
1231            .split("pub fn threshold_sign<R>")
1232            .nth(1)
1233            .unwrap()
1234            .split("pub fn threshold_sign_zeroizing<R>")
1235            .next()
1236            .unwrap();
1237        assert!(legacy_signing.contains("Zeroizing::new(share)"));
1238        assert!(legacy_signing.contains("threshold_sign_zeroizing("));
1239        assert!(
1240            !legacy_signing.contains('?'),
1241            "all consumed legacy shares must be guarded before any fallible signing work"
1242        );
1243        assert!(
1244            !derive_before(signing_source, "pub struct RootSigningNonces").contains("Clone"),
1245            "RootSigningNonces must not regain secret duplication through Clone"
1246        );
1247        assert_eq!(
1248            dkg_source
1249                .matches("#[serde(deserialize_with = \"deserialize_zeroizing_bytes\")]")
1250                .count(),
1251            3,
1252            "all three direct DKG secret-byte fields must use the shared visitor"
1253        );
1254        assert_eq!(
1255            dkg_source
1256                .matches("#[serde(deserialize_with = \"deserialize_zeroizing_byte_map\")]")
1257                .count(),
1258            1,
1259            "the recipient-bound round-two map must use the shared byte visitor"
1260        );
1261        assert_eq!(
1262            signing_source
1263                .matches("#[serde(deserialize_with = \"deserialize_zeroizing_bytes\")]")
1264                .count(),
1265            1,
1266            "signing nonces must use the shared visitor"
1267        );
1268        let complete_dkg = dkg_source
1269            .split("pub fn run_complete_dkg")
1270            .nth(1)
1271            .expect("complete DKG implementation")
1272            .split("#[cfg(test)]")
1273            .next()
1274            .expect("complete DKG ends before tests");
1275        assert!(complete_dkg.contains("BTreeMap<u16, Zeroizing<RootDkgRound1Output>>"));
1276        assert!(complete_dkg.contains("BTreeMap<u16, Zeroizing<RootKeyPackage>>"));
1277        let public_ready = complete_dkg
1278            .find("require_first_public_key_package(public_key_package)?")
1279            .expect("public result validated before releasing guarded private map");
1280        let private_handoff = complete_dkg
1281            .find("let key_packages = key_packages")
1282            .expect("final legacy private-map handoff");
1283        assert!(public_ready < private_handoff);
1284        let finalize = dkg_source
1285            .split("pub fn dkg_finalize_participant_zeroizing")
1286            .nth(1)
1287            .unwrap()
1288            .split("fn deserialize_round1_packages")
1289            .next()
1290            .unwrap();
1291        assert!(
1292            finalize.find("serialize_public_key_package(").unwrap()
1293                < finalize.find("RootKeyPackage::from_zeroizing(").unwrap(),
1294            "serialized private key must stay guarded through fallible public serialization"
1295        );
1296        for forbidden in [
1297            "recipient_packages.insert(*identifier, package.clone())",
1298            "round2_by_recipient[&first_identifier].clone()",
1299            "round2_by_recipient[&identifier].clone()",
1300        ] {
1301            assert!(
1302                !complete_dkg.contains(forbidden),
1303                "complete DKG must move each recipient secret exactly once: {forbidden}"
1304            );
1305        }
1306        assert!(
1307            complete_dkg.contains("for (recipient, package) in round2_packages"),
1308            "complete DKG must consume outbound recipient packages"
1309        );
1310        assert!(
1311            complete_dkg.contains("round2_by_recipient.remove(&identifier)"),
1312            "complete DKG must remove each recipient map for finalization"
1313        );
1314    }
1315
1316    #[test]
1317    fn secret_dkg_deserializers_reject_partially_decoded_json_and_cbor() {
1318        #[derive(Serialize)]
1319        struct LateInvalidParticipant {
1320            key_package: RootKeyPackage,
1321            public_key_package: bool,
1322        }
1323        #[derive(Serialize)]
1324        struct LateInvalidDkg {
1325            key_packages: BTreeMap<u16, RootKeyPackage>,
1326            public_key_package: bool,
1327        }
1328        #[derive(Serialize)]
1329        struct LateInvalidDkgMap {
1330            key_packages: BTreeMap<u16, MalformedKeyPackage>,
1331            public_key_package: bool,
1332        }
1333        let key_fixture = RootKeyPackage {
1334            frost_identifier: 7,
1335            key_package: vec![1, 2, 3],
1336        };
1337        let participant = LateInvalidParticipant {
1338            key_package: key_fixture.clone(),
1339            public_key_package: false,
1340        };
1341        let dkg = LateInvalidDkg {
1342            key_packages: BTreeMap::from([(7, key_fixture)]),
1343            public_key_package: false,
1344        };
1345        assert!(
1346            serde_json::from_slice::<RootParticipantDkgOutput>(
1347                &serde_json::to_vec(&participant).expect("late invalid participant JSON")
1348            )
1349            .is_err()
1350        );
1351        assert!(
1352            ciborium::from_reader::<RootParticipantDkgOutput, _>(
1353                cbor_bytes(&participant).as_slice()
1354            )
1355            .is_err()
1356        );
1357        assert!(
1358            serde_json::from_slice::<RootDkgOutput>(
1359                &serde_json::to_vec(&dkg).expect("late invalid DKG JSON")
1360            )
1361            .is_err()
1362        );
1363        assert!(ciborium::from_reader::<RootDkgOutput, _>(cbor_bytes(&dkg).as_slice()).is_err());
1364        let partial_map = LateInvalidDkgMap {
1365            key_packages: BTreeMap::from([
1366                (
1367                    7,
1368                    MalformedKeyPackage {
1369                        frost_identifier: 7,
1370                        key_package: vec![serde_json::json!(1), serde_json::json!(2)],
1371                    },
1372                ),
1373                (
1374                    8,
1375                    MalformedKeyPackage {
1376                        frost_identifier: 8,
1377                        key_package: vec![serde_json::json!(3), serde_json::json!("invalid")],
1378                    },
1379                ),
1380            ]),
1381            public_key_package: false,
1382        };
1383        assert!(
1384            serde_json::from_slice::<RootDkgOutput>(
1385                &serde_json::to_vec(&partial_map).expect("partial map JSON")
1386            )
1387            .is_err()
1388        );
1389        assert!(
1390            ciborium::from_reader::<RootDkgOutput, _>(cbor_bytes(&partial_map).as_slice()).is_err()
1391        );
1392        let invalid_bytes = vec![
1393            serde_json::json!(1),
1394            serde_json::json!(2),
1395            serde_json::json!("bad"),
1396            serde_json::json!(3),
1397        ];
1398        let key = MalformedKeyPackage {
1399            frost_identifier: 7,
1400            key_package: invalid_bytes.clone(),
1401        };
1402        let round1 = MalformedRound1Output {
1403            frost_identifier: 7,
1404            round1_secret_package: invalid_bytes.clone(),
1405            round1_package: vec![3],
1406        };
1407        let round2_secret = MalformedRound2SecretOutput {
1408            frost_identifier: 7,
1409            round2_secret_package: invalid_bytes.clone(),
1410            round2_packages: BTreeMap::from([(8, vec![3])]),
1411        };
1412        let round2_map = MalformedRound2MapOutput {
1413            frost_identifier: 7,
1414            round2_secret_package: vec![1, 2, 3],
1415            round2_packages: BTreeMap::from([(8, invalid_bytes)]),
1416        };
1417
1418        let key_json = serde_json::to_vec(&key).expect("key malformed JSON fixture");
1419        let round1_json = serde_json::to_vec(&round1).expect("round-one malformed JSON fixture");
1420        let round2_secret_json =
1421            serde_json::to_vec(&round2_secret).expect("round-two secret malformed JSON fixture");
1422        let round2_map_json =
1423            serde_json::to_vec(&round2_map).expect("round-two map malformed JSON fixture");
1424        assert!(serde_json::from_slice::<RootKeyPackage>(&key_json).is_err());
1425        assert!(serde_json::from_slice::<RootDkgRound1Output>(&round1_json).is_err());
1426        assert!(serde_json::from_slice::<RootDkgRound2Output>(&round2_secret_json).is_err());
1427        assert!(serde_json::from_slice::<RootDkgRound2Output>(&round2_map_json).is_err());
1428
1429        let key_cbor = cbor_bytes(&key);
1430        let round1_cbor = cbor_bytes(&round1);
1431        let round2_secret_cbor = cbor_bytes(&round2_secret);
1432        let round2_map_cbor = cbor_bytes(&round2_map);
1433        assert!(ciborium::from_reader::<RootKeyPackage, _>(key_cbor.as_slice()).is_err());
1434        assert!(ciborium::from_reader::<RootDkgRound1Output, _>(round1_cbor.as_slice()).is_err());
1435        assert!(
1436            ciborium::from_reader::<RootDkgRound2Output, _>(round2_secret_cbor.as_slice()).is_err()
1437        );
1438        assert!(
1439            ciborium::from_reader::<RootDkgRound2Output, _>(round2_map_cbor.as_slice()).is_err()
1440        );
1441    }
1442
1443    #[test]
1444    fn secret_dkg_deserializers_round_trip_all_secret_carriers() {
1445        let mut key_fixture = RootKeyPackage {
1446            frost_identifier: 7,
1447            key_package: vec![0xde, 0xad, 0xbe, 0xef],
1448        };
1449        let public_key_package = RootPublicKeyPackage {
1450            public_key_package: vec![1],
1451            root_public_key: vec![2],
1452            verifying_shares: BTreeMap::from([(7, vec![3])]),
1453        };
1454        let participant = RootParticipantDkgOutput {
1455            key_package: key_fixture.clone(),
1456            public_key_package: public_key_package.clone(),
1457        };
1458        let participant_json = serde_json::to_vec(&participant).expect("participant JSON");
1459        let participant_cbor = cbor_bytes(&participant);
1460        let guarded: Zeroizing<RootParticipantDkgOutput> =
1461            serde_json::from_slice(&participant_json).expect("guarded participant JSON");
1462        assert_eq!(*guarded, participant);
1463        assert_eq!(
1464            serde_json::to_vec(&guarded).expect("guarded JSON"),
1465            participant_json
1466        );
1467        assert_eq!(cbor_bytes(&guarded), participant_cbor);
1468        let guarded_cbor: Zeroizing<RootParticipantDkgOutput> =
1469            ciborium::from_reader(participant_cbor.as_slice()).expect("guarded participant CBOR");
1470        assert_eq!(*guarded_cbor, participant);
1471        let dkg = RootDkgOutput {
1472            key_packages: BTreeMap::from([(7, key_fixture.clone())]),
1473            public_key_package,
1474        };
1475        assert_eq!(
1476            serde_json::from_slice::<RootDkgOutput>(
1477                &serde_json::to_vec(&dkg).expect("complete DKG JSON")
1478            )
1479            .expect("complete DKG JSON round trip"),
1480            dkg
1481        );
1482        assert_eq!(
1483            ciborium::from_reader::<RootDkgOutput, _>(cbor_bytes(&dkg).as_slice())
1484                .expect("complete DKG CBOR round trip"),
1485            dkg
1486        );
1487        fn assert_zeroize_on_drop<T: zeroize::ZeroizeOnDrop>(_: &T) {}
1488        assert_zeroize_on_drop(&guarded);
1489        let key_json =
1490            Zeroizing::new(serde_json::to_vec(&key_fixture).expect("key-package JSON fixture"));
1491        let mut key_json_round_trip = serde_json::from_slice::<RootKeyPackage>(&key_json)
1492            .expect("key-package JSON secret bytes");
1493        assert_eq!(key_json_round_trip, key_fixture);
1494        let key_cbor = Zeroizing::new(cbor_bytes(&key_fixture));
1495        let mut key_cbor_round_trip =
1496            ciborium::from_reader::<RootKeyPackage, _>(key_cbor.as_slice())
1497                .expect("key-package CBOR secret bytes");
1498        assert_eq!(key_cbor_round_trip, key_fixture);
1499
1500        let mut round1_fixture = RootDkgRound1Output {
1501            frost_identifier: 7,
1502            round1_secret_package: vec![0xca, 0xfe, 0xba, 0xbe],
1503            round1_package: vec![1, 2, 3],
1504        };
1505        let round1_json =
1506            Zeroizing::new(serde_json::to_vec(&round1_fixture).expect("round-one JSON fixture"));
1507        let mut round1_json_round_trip =
1508            serde_json::from_slice::<RootDkgRound1Output>(&round1_json)
1509                .expect("round-one JSON secret bytes");
1510        assert_eq!(round1_json_round_trip, round1_fixture);
1511        let round1_cbor = Zeroizing::new(cbor_bytes(&round1_fixture));
1512        let mut round1_cbor_round_trip =
1513            ciborium::from_reader::<RootDkgRound1Output, _>(round1_cbor.as_slice())
1514                .expect("round-one CBOR secret bytes");
1515        assert_eq!(round1_cbor_round_trip, round1_fixture);
1516
1517        let mut round2_fixture = RootDkgRound2Output {
1518            frost_identifier: 7,
1519            round2_secret_package: vec![1, 2, 3],
1520            round2_packages: BTreeMap::from([(8, vec![4, 5, 6]), (9, vec![7, 8, 9])]),
1521        };
1522
1523        let json =
1524            Zeroizing::new(serde_json::to_vec(&round2_fixture).expect("round-two JSON fixture"));
1525        let mut json_round_trip = serde_json::from_slice::<RootDkgRound2Output>(&json)
1526            .expect("round-two JSON secret map");
1527        assert_eq!(json_round_trip, round2_fixture);
1528
1529        let cbor = Zeroizing::new(cbor_bytes(&round2_fixture));
1530        let mut cbor_round_trip = ciborium::from_reader::<RootDkgRound2Output, _>(cbor.as_slice())
1531            .expect("round-two CBOR secret map");
1532        assert_eq!(cbor_round_trip, round2_fixture);
1533
1534        for secret_carrier in [
1535            &mut key_fixture,
1536            &mut key_json_round_trip,
1537            &mut key_cbor_round_trip,
1538        ] {
1539            secret_carrier.zeroize();
1540            assert!(secret_carrier.key_package.is_empty());
1541        }
1542        for secret_carrier in [
1543            &mut round1_fixture,
1544            &mut round1_json_round_trip,
1545            &mut round1_cbor_round_trip,
1546        ] {
1547            secret_carrier.zeroize();
1548            assert!(secret_carrier.round1_secret_package.is_empty());
1549        }
1550        for secret_carrier in [
1551            &mut round2_fixture,
1552            &mut json_round_trip,
1553            &mut cbor_round_trip,
1554        ] {
1555            secret_carrier.zeroize();
1556            assert!(secret_carrier.round2_secret_package.is_empty());
1557            assert!(secret_carrier.round2_packages.is_empty());
1558        }
1559    }
1560
1561    #[test]
1562    fn secret_dkg_deserializer_reports_expected_container_shapes() {
1563        let byte_sequence_error = serde_json::from_str::<RootKeyPackage>(
1564            r#"{"frost_identifier":7,"key_package":"not-a-sequence"}"#,
1565        )
1566        .expect_err("a root key secret must be encoded as a byte sequence");
1567        assert!(
1568            byte_sequence_error
1569                .to_string()
1570                .contains("a sequence of root secret bytes"),
1571            "unexpected byte-sequence diagnostic: {byte_sequence_error}"
1572        );
1573
1574        let recipient_map_error = serde_json::from_str::<RootDkgRound2Output>(
1575            r#"{"frost_identifier":7,"round2_secret_package":[1,2,3],"round2_packages":[]}"#,
1576        )
1577        .expect_err("recipient-bound secrets must be encoded as a map");
1578        assert!(
1579            recipient_map_error
1580                .to_string()
1581                .contains("a map of recipient-bound root secret byte sequences"),
1582            "unexpected recipient-map diagnostic: {recipient_map_error}"
1583        );
1584    }
1585
1586    #[test]
1587    fn secret_dkg_json_and_cbor_match_legacy_vec_wire_layout() {
1588        let key_secret = vec![0xde, 0xad, 0xbe, 0xef];
1589        let key_package = RootKeyPackage {
1590            frost_identifier: 7,
1591            key_package: key_secret.clone(),
1592        };
1593        let legacy_key = LegacyRootKeyPackage {
1594            frost_identifier: 7,
1595            key_package: &key_secret,
1596        };
1597        assert_eq!(
1598            serde_json::to_vec(&key_package).expect("key JSON"),
1599            serde_json::to_vec(&legacy_key).expect("legacy key JSON")
1600        );
1601        assert_eq!(cbor_bytes(&key_package), cbor_bytes(&legacy_key));
1602
1603        let round1_secret = vec![0xca, 0xfe, 0xba, 0xbe];
1604        let round1_public = vec![1, 2, 3];
1605        let round1 = RootDkgRound1Output {
1606            frost_identifier: 7,
1607            round1_secret_package: round1_secret.clone(),
1608            round1_package: round1_public.clone(),
1609        };
1610        let legacy_round1 = LegacyRound1Output {
1611            frost_identifier: 7,
1612            round1_secret_package: &round1_secret,
1613            round1_package: &round1_public,
1614        };
1615        assert_eq!(
1616            serde_json::to_vec(&round1).expect("round-one JSON"),
1617            serde_json::to_vec(&legacy_round1).expect("legacy round-one JSON")
1618        );
1619        assert_eq!(cbor_bytes(&round1), cbor_bytes(&legacy_round1));
1620
1621        let round2_secret = vec![0x12, 0x34, 0x56, 0x78];
1622        let round2_public = BTreeMap::from([(8, vec![4, 5, 6])]);
1623        let round2 = RootDkgRound2Output {
1624            frost_identifier: 7,
1625            round2_secret_package: round2_secret.clone(),
1626            round2_packages: round2_public.clone(),
1627        };
1628        let legacy_round2 = LegacyRound2Output {
1629            frost_identifier: 7,
1630            round2_secret_package: &round2_secret,
1631            round2_packages: &round2_public,
1632        };
1633        assert_eq!(
1634            serde_json::to_vec(&round2).expect("round-two JSON"),
1635            serde_json::to_vec(&legacy_round2).expect("legacy round-two JSON")
1636        );
1637        assert_eq!(cbor_bytes(&round2), cbor_bytes(&legacy_round2));
1638    }
1639
1640    #[test]
1641    fn identifier_value_rejects_unrostered_identifier() {
1642        let config = test_config();
1643        let identifier = frost_identifier(14).expect("identifier");
1644        assert!(identifier_value(&config, identifier).is_err());
1645    }
1646
1647    #[test]
1648    fn frost_identifier_rejects_zero_identifier() {
1649        let error = frost_identifier(0).expect_err("zero identifier");
1650        assert!(error.to_string().contains("frost operation failed"));
1651    }
1652
1653    #[test]
1654    fn rostered_identifier_and_encoding_helpers_are_diagnostic() {
1655        let config = test_config();
1656        let identifier = rostered_frost_identifier(&config, 1, "unit").expect("rostered");
1657        assert_eq!(identifier, frost_identifier(1).expect("identifier"));
1658
1659        let error =
1660            rostered_frost_identifier(&config, 14, "unit").expect_err("unrostered certifier");
1661        assert!(
1662            error
1663                .to_string()
1664                .contains("unit certifier 14 is not rostered")
1665        );
1666
1667        let encoding_error = frost_encoding_error("unit failure");
1668        assert!(
1669            encoding_error
1670                .to_string()
1671                .contains("FROST artifact canonical encoding failed")
1672        );
1673
1674        let encoded = serialize_frost(&7u16).expect("serialize");
1675        let decoded: u16 = deserialize_frost(encoded.as_slice()).expect("deserialize");
1676        assert_eq!(decoded, 7);
1677        assert!(deserialize_frost::<u16>(b"not cbor").is_err());
1678    }
1679
1680    #[test]
1681    fn peer_package_filter_retains_every_non_excluded_package() {
1682        let mut packages = BTreeMap::new();
1683        packages.insert(1, b"one".to_vec());
1684        packages.insert(2, b"two".to_vec());
1685        packages.insert(3, b"three".to_vec());
1686
1687        let peers = peer_packages_except(&packages, 2);
1688
1689        assert_eq!(peers.len(), 2);
1690        assert_eq!(peers.get(&1).expect("peer one"), b"one");
1691        assert_eq!(peers.get(&3).expect("peer three"), b"three");
1692        assert!(!peers.contains_key(&2));
1693    }
1694
1695    #[test]
1696    fn complete_dkg_fail_closed_helpers_reject_missing_assembly_state() {
1697        let recipient_error = require_recipient_round2_packages(None, 7)
1698            .expect_err("a missing recipient package set must abort local DKG assembly");
1699        assert_eq!(
1700            recipient_error,
1701            RootError::Frost {
1702                detail: "missing recipient-bound round-two packages for 7".to_owned()
1703            }
1704        );
1705
1706        let public_error = require_first_public_key_package(None)
1707            .expect_err("a missing first-participant public package must abort assembly");
1708        assert_eq!(
1709            public_error,
1710            RootError::Frost {
1711                detail: "missing first participant public key package".to_owned()
1712            }
1713        );
1714    }
1715
1716    #[test]
1717    fn round_one_and_complete_dkg_success_paths_are_diagnostic() {
1718        let config = test_config();
1719        let mut rng = StdRng::seed_from_u64(11);
1720
1721        let round1 = dkg_round1(&config, 1, &mut rng).expect("round one");
1722        assert_eq!(round1.frost_identifier, 1);
1723        assert!(!round1.round1_secret_package.is_empty());
1724        assert!(!round1.round1_package.is_empty());
1725
1726        let dkg = run_complete_dkg(&config, &mut rng).expect("complete dkg");
1727        assert_eq!(dkg.key_packages.len(), usize::from(config.max_signers));
1728        assert_eq!(
1729            dkg.public_key_package.verifying_shares.len(),
1730            usize::from(config.max_signers)
1731        );
1732    }
1733
1734    #[test]
1735    fn deserialize_peer_package_helpers_reject_bad_sender_sets() {
1736        let config = test_config();
1737        let participant = frost_identifier(1).expect("participant");
1738
1739        assert!(
1740            deserialize_round1_packages(&config, participant, BTreeMap::new())
1741                .expect("empty round-one helper input")
1742                .is_empty()
1743        );
1744        assert!(
1745            deserialize_round2_packages(&config, participant, BTreeMap::new())
1746                .expect("empty round-two helper input")
1747                .is_empty()
1748        );
1749
1750        let mut nonrostered_round1 = BTreeMap::new();
1751        nonrostered_round1.insert(14, Vec::new());
1752        assert!(deserialize_round1_packages(&config, participant, nonrostered_round1).is_err());
1753
1754        let mut self_round1 = BTreeMap::new();
1755        self_round1.insert(1, Vec::new());
1756        assert!(deserialize_round1_packages(&config, participant, self_round1).is_err());
1757
1758        let mut malformed_round1 = BTreeMap::new();
1759        malformed_round1.insert(2, b"not a round-one package".to_vec());
1760        assert!(deserialize_round1_packages(&config, participant, malformed_round1).is_err());
1761
1762        let mut nonrostered_round2 = BTreeMap::new();
1763        nonrostered_round2.insert(14, Zeroizing::new(Vec::new()));
1764        assert!(deserialize_round2_packages(&config, participant, nonrostered_round2).is_err());
1765
1766        let mut self_round2 = BTreeMap::new();
1767        self_round2.insert(1, Zeroizing::new(Vec::new()));
1768        assert!(deserialize_round2_packages(&config, participant, self_round2).is_err());
1769
1770        let mut malformed_round2 = BTreeMap::new();
1771        malformed_round2.insert(2, Zeroizing::new(b"not a round-two package".to_vec()));
1772        assert!(deserialize_round2_packages(&config, participant, malformed_round2).is_err());
1773    }
1774
1775    #[test]
1776    fn serialize_public_key_package_rejects_missing_verification_share() {
1777        let config = test_config();
1778        let mut rng = StdRng::seed_from_u64(7);
1779        let dkg = run_complete_dkg(&config, &mut rng).expect("dkg");
1780        let public: frost::keys::PublicKeyPackage =
1781            deserialize_frost(dkg.public_key_package.public_key_package.as_slice())
1782                .expect("public package");
1783        let mut changed_config = config;
1784        changed_config.certifiers[0].frost_identifier = 14;
1785        assert!(serialize_public_key_package(&changed_config, &public).is_err());
1786    }
1787}