Skip to main content

commonware_cryptography/bls12381/dkg/
feldman_desmedt.rs

1//! Feldman/Desmedt Distributed Key Generation (DKG) and Resharing for BLS12-381.
2//!
3//! This module implements a Feldman/Desmedt-style DKG and Resharing protocol. Unlike other
4//! constructions, this construction does not require encrypted shares to be publicly broadcast to
5//! complete a DKG/Reshare. Shares, instead, are sent directly between dealers and players over an
6//! encrypted channel (which can be instantiated with
7//! [commonware-p2p](https://docs.rs/commonware-p2p)).
8//!
9//! The DKG is based on the "Joint-Feldman" construction from "Secure Distributed Key
10//! Generation for Discrete-Log Based Cryptosystems" (GJKR99) and Resharing is based
11//! on the construction described in "Redistributing secret shares to new access structures
12//! and its applications" (Desmedt97).
13//!
14//! # Overview
15//!
16//! The protocol involves _dealers_ and _players_. The dealers are trying to jointly create a shared
17//! key, and then distribute it among the players. The dealers may have pre-existing shares of a key
18//! from a previous round, in which case the goal is to re-distribute that key among the players,
19//! with fresh randomness.
20//!
21//! The protocol is also designed such that an external observer can figure out whether the protocol
22//! succeeded or failed, and learn of the public outputs of the protocol. This includes
23//! the participants in the protocol, and the public polynomial committing to the key
24//! and its sharing.
25//!
26//! # Usage
27//!
28//! ## Core Types
29//!
30//! * [`Info`]: Configuration for a DKG/Reshare round, containing the dealers, players, and optional previous output
31//! * [`Output`]: The public result of a successful DKG round, containing the public polynomial and player list
32//! * [`Share`]: A player's final private share of the distributed key (from `primitives::group`)
33//! * [`Dealer`]: State machine for a dealer participating in the protocol
34//! * [`Player`]: State machine for a player receiving dealings
35//! * [`SignedDealerLog`]: A dealer's signed transcript of their interactions with players
36//!
37//! ## Message Types
38//!
39//! * [`DealerPubMsg`]: Public commitment polynomial sent from dealer to all players
40//! * [`DealerPrivMsg`]: Private dealing sent from dealer to a specific player
41//! * [`PlayerAck`]: Acknowledgement sent from player back to dealer
42//! * [`DealerLog`]: Complete log of a dealer's interactions (commitments and acks/reveals)
43//!
44//! ## Protocol Flow
45//!
46//! ### Step 1: Initialize Round
47//!
48//! Create a [`Info`] using [`Info::new`] with:
49//! - Round number (should increment sequentially, including for failed rounds)
50//! - Optional previous [`Output`] (for resharing)
51//! - List of dealers (must be >= quorum of previous round if resharing)
52//! - List of players who will receive dealings
53//!
54//! ### Step 2: Dealer Phase
55//!
56//! Each dealer calls [`Dealer::start`] which returns:
57//! - A [`Dealer`] instance for tracking state
58//! - A [`DealerPubMsg`] containing the polynomial commitment to broadcast
59//! - A vector of `(player_id, DealerPrivMsg)` pairs to send privately
60//!
61//! The [`DealerPubMsg`] contains a public polynomial commitment of degree `2f` where `f = max_faults(n)`.
62//! Each [`DealerPrivMsg`] contains a scalar evaluation of the dealer's private polynomial at the player's index.
63//!
64//! ### Step 3: Player Verification
65//!
66//! Each player creates a [`Player`] instance via [`Player::new`], then for each dealer message:
67//! - Call [`Player::dealer_message`] with the [`DealerPubMsg`] and [`DealerPrivMsg`]
68//! - If valid, this returns a [`PlayerAck`] containing a signature over `(dealer, commitment)`
69//! - The player verifies that the private dealing matches the public commitment evaluation
70//!
71//! ### Step 4: Dealer Collection
72//!
73//! Each dealer:
74//! - Calls [`Dealer::receive_player_ack`] for each acknowledgement received
75//! - After timeout, calls [`Dealer::finalize`] to produce a [`SignedDealerLog`]
76//! - The log contains the commitment and either acks or reveals for each player
77//!
78//! ### Step 5: Finalization
79//!
80//! With collected [`SignedDealerLog`]s:
81//! - Call [`SignedDealerLog::check`] to verify and extract [`DealerLog`]s
82//! - Players call [`Player::finalize`] with all logs to compute their [`Share`] and [`Output`]
83//! - Observers call [`observe`] with all logs to compute just the [`Output`]
84//!
85//! The [`Output`] contains:
86//! - The final public polynomial (sum of dealer polynomials for DKG, interpolation for reshare),
87//! - The list of dealers who distributed dealings,
88//! - The list of players who received shares,
89//! - The set of players whose shares may have been revealed,
90//! - A digest of the round's [`Info`] (including the counter, and the list of dealers and players).
91//!
92//! ## Trusted Dealing Functions
93//!
94//! As a convenience (for tests, etc.), this module also provides functions for
95//! generating shares using a trusted dealer.
96//!
97//! - [`deal`]: given a list of players, generates an [`Output`] like the DKG would,
98//! - [`deal_anonymous`]: a lower-level version that produces a polynomial directly,
99//!   and doesn't require public keys for the players.
100//!
101//! ## State
102//!
103//! The structs in this module are stateful and they assume that they exist from the
104//! start of the DKG to the end of the DKG.
105//!
106//! During restart, state should be restored by replaying all messages that
107//! dealers and players previously processed. For the dealer, it's important to use a
108//! seeded form of randomness, so that way the same messages can be generated on a second run.
109//! For the player, using [`Player::resume`] is more robust than just [`Player::new`], because it
110//! checks the integrity of the replayed messages against the publicly committed transcript (so far).
111//! This can detect some recoverable operator errors, like storage misconfiguration (where a player has publicly
112//! acknowledged a private message but has no record of it in storage).
113//!
114//! ## Errors and Failures
115//!
116//! [`enum@Error`] reports invalid caller input or incomplete local state.
117//! [`DealerMessageError`] and [`PlayerAckError`] explain why live messages were
118//! rejected without attributing the failure to a participant. [`FaultReason`]
119//! describes invalid content in a configured dealer's signed log. [`Failure`]
120//! reports that the protocol round did not produce the requested result.
121//!
122//! These categories reflect the evidence available to each operation. A
123//! live-message error explains why input was rejected but does not establish who
124//! caused it. A [`FaultReason`] supports dealer attribution only for invalid
125//! content in a configured dealer's log authenticated by [`SignedDealerLog::check`].
126//!
127//! [`Failure::InsufficientLogs`] separates those faults from configured dealers
128//! with no usable log. A safe [`DealerLogSummary::TooManyReveals`] log is
129//! unavailable because it intentionally omits its results.
130//!
131//! [`Player::finalize`] can encounter either kind of problem, so it returns
132//! [`FinalizeError`] to distinguish a local [`enum@Error`] from a protocol [`Failure`].
133//!
134//! # Caveats
135//!
136//! ## Share Reveals
137//!
138//! In order to prevent malicious dealers from withholding shares from players, we
139//! require the dealers reveal the shares for which they did not receive acks.
140//!
141//! Under synchrony (as discussed below), this will only happen if either:
142//! - the dealer is malicious, not sending a share, but honestly revealing,
143//! - or, the player is malicious, not sending an ack when they should.
144//!
145//! ### Up to `f` Reveals Under Synchrony
146//!
147//! Under synchrony (where `t` is the maximum amount of time it takes for a message to be sent between any two participants),
148//! this construction will not result in more than `f` reveals from honest dealers, and none of those reveals are for honest players
149//! (`2f + 1` commitments with at most `f` players are Byzantine).
150//!
151//! To see how this is true, first consider that in any successful round there must exist `2f + 1` commitments each with at most `f`
152//! reveals. This implies that all players must have acknowledged or have access to a reveal for each of the `2f + 1` selected commitments
153//! (allowing them to derive their share). Next, consider that when the network is synchronous that all `2f + 1` honest players send
154//! acknowledgements to honest dealers before `2t`. Because `2f + 1` commitments must be chosen, at least `f + 1` commitments
155//! must be from honest dealers (where no honest player dealing is revealed...recall, a Byzantine dealer can opt to reveal any
156//! player's dealing even if they sent an acknowledgement).
157//!
158//! Even if the remaining `f` commitments are from Byzantine dealers, there will not be enough dealings to recover the derived share
159//! of any honest player (at most `f` of `2f + 1` points for a linear combination publicly revealed). Given all `2f + 1`
160//! honest players have access to their shares and it is not possible for a Byzantine player to derive any honest player's share, this claim holds.
161//!
162//! ### Up to `2f` Reveals Under Asynchrony
163//!
164//! If the network is asynchronous, Byzantine players may obtain up to `2f` revealed shares (`f` from Byzantine players
165//! and `f` from honest players).
166//!
167//! To see how this could be, consider a network where `f` honest participants are in one partition and (`f + 1` honest and
168//! `f` Byzantine participants) are in another. All `f` Byzantine players acknowledge dealings from the `f + 1` honest dealers.
169//! Participants in the second partition will complete a round and all the reveals will belong to the same set of `f`
170//! honest players (that are in the first partition). A colluding Byzantine adversary will then have access to their acknowledged `f`
171//! shares and the revealed `f` shares. If the Byzantine adversary reveals all of their (still private) shares at this time, each of the
172//! `f + 1` honest players that were in the second partition will be able to derive the shared secret without collusion (using their private share
173//! and the `2f` revealed shares). **It will not be possible for any external observer (or a Byzantine adversary), however, to recover the shared secret.**
174//!
175//! While not entirely revealed, a secret with more than `f` revealed shares may no longer be safe for some applications (like when used to
176//! form threshold certificates for consensus). Consider an equivocating leader (one of the `f` Byzantine players) that sends one block `B_1` to `f`
177//! honest players and another block `B_2` to `f + 1` other honest players. Normally, it would only be possible to create one quorum of `2f + 1` (for `B_2`),
178//! however, with `h` other shares revealed another quorum of `2f + h` can be formed for `B_1`.
179//!
180//! #### Dropping the Synchrony Assumption for `f` Bounded Reveals?
181//!
182//! It is possible to design a DKG/Resharing scheme that maintains a shared secret where at least `f + 1` honest players
183//! must participate to recover the shared secret that doesn't require a synchrony assumption (`2f + 1` threshold
184//! where at most `f` players are Byzantine) by combining encryption and ZK Proofs. We have an implementation of one
185//! such protocol, [Golden](https://eprint.iacr.org/2025/1924), in [`crate::bls12381::dkg::golden`].
186//!
187//! ## Handling Complaints
188//!
189//! This crate does not provide an integrated mechanism for tracking complaints from players (of malicious dealers). However, it is
190//! possible to implement your own mechanism and to manually disqualify dealers from a given round in the arbiter. This decision was made
191//! because the mechanism for communicating commitments/shares/acknowledgements is highly dependent on the context in which this
192//! construction is used.
193//!
194//! In practice:
195//! - [`Player::dealer_message`] returns [`DealerMessageError`] for an invalid
196//!   message and `Ok(None)` for a benign duplicate
197//! - [`Dealer::receive_player_ack`] returns [`PlayerAckError`] when an
198//!   acknowledgement cannot be used
199//! - Other custom mechanisms can exclude dealers before calling [`observe`] or [`Player::finalize`],
200//!   to enforce other rules for "misbehavior" beyond what the DKG does already.
201//!
202//! ## Aggregate Degree
203//!
204//! For `n` players and at most `f` faults, the configured quorum is `n - f`, so dealer polynomials
205//! have degree `d = n - f - 1`. The aggregate must retain that exact degree; otherwise, fewer than
206//! `n - f` participants could recover the shared secret or produce a threshold signature.
207//!
208//! Every accepted dealer commitment has exact degree `d`, and a usable dealer log records one
209//! result for every player. At most `f` results can be Byzantine acknowledgements issued without
210//! verifying their shares. Every other result is either an honest acknowledgement of a verified
211//! share or a reveal verified against the commitment. The log therefore contains at least
212//! `n - f = d + 1` valid scalar evaluations, enough to determine the dealer's polynomial and the
213//! scalar behind every coefficient commitment.
214//!
215//! Whether selected commitments are summed during initial key generation or combined with non-zero
216//! Lagrange weights during resharing, the leading coefficient includes an independently sampled
217//! contribution from at least one honest dealer. Although
218//! [a rushing adversary](https://decentralizedthoughts.github.io/2019-06-07-modeling-the-adversary/#rushing)
219//! may observe the honest commitments before choosing its own, it must know every Byzantine
220//! coefficient scalar before those commitments can be selected. Choosing those scalars so their
221//! combined leading term cancels the honest contribution would require solving the discrete
222//! logarithm of the honest leading commitment (preventing any efficient adversary from forcing the
223//! aggregate below degree `d`).
224//!
225//! ## Non-Uniform Distribution
226//!
227//! The Joint-Feldman DKG protocol does not guarantee a uniformly random secret key is generated. An adversary
228//! can introduce `O(lg N)` bits of bias into the key with `O(poly(N))` amount of computation. For uses
229//! like signing, threshold encryption, where the security of the scheme reduces to that of
230//! the underlying assumption that cryptographic constructions using the curve are secure (i.e.
231//! that the Discrete Logarithm Problem, or stronger variants, are hard), then this caveat does
232//! not affect the security of the scheme. This must be taken into account when integrating this
233//! component into more esoteric schemes.
234//!
235//! This choice was explicitly made, because the best known protocols guaranteeing a uniform output
236//! require an extra round of broadcast ([GJKR02](https://www.researchgate.net/publication/2558744_Revisiting_the_Distributed_Key_Generation_for_Discrete-Log_Based_Cryptosystems),
237//! [BK25](https://eprint.iacr.org/2025/819)).
238//!
239//! # Example
240//!
241//! ```
242//! use commonware_cryptography::bls12381::{
243//!     dkg::feldman_desmedt::{
244//!         Dealer, Info, Logs, Player, Reveal, SignedDealerLog, observe,
245//!     },
246//!     primitives::{variant::MinSig, sharing::Mode},
247//! };
248//! use commonware_cryptography::{ed25519, Signer};
249//! use commonware_math::algebra::Random;
250//! use commonware_utils::{ordered::Set, TryCollect, N3f1};
251//! use std::collections::BTreeMap;
252//! use rand::SeedableRng;
253//! use rand_chacha::ChaCha8Rng;
254//!
255//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
256//! let mut rng = ChaCha8Rng::seed_from_u64(42);
257//!
258//! // Generate 4 Ed25519 private keys for participants
259//! let mut private_keys = Vec::new();
260//! for _ in 0..4 {
261//!     let private_key = ed25519::PrivateKey::random(&mut rng);
262//!     private_keys.push(private_key);
263//! }
264//!
265//! // All 4 participants are both dealers and players in initial DKG
266//! let dealer_set: Set<ed25519::PublicKey> = private_keys.iter()
267//!     .map(|k| k.public_key())
268//!     .try_collect()?;
269//! let player_set = dealer_set.clone();
270//!
271//! // Step 1: Create round info for initial DKG
272//! let info = Info::<MinSig, ed25519::PublicKey>::new::<N3f1>(
273//!     b"application-namespace",
274//!     0,                        // round number
275//!     None,                     // no previous output (initial DKG)
276//!     Mode::NonZeroCounter,     // sharing mode
277//!     Reveal::V1,               // revealed-share calculation
278//!     dealer_set.clone(),       // dealers
279//!     player_set.clone(),       // players
280//! )?;
281//!
282//! // Step 2: Initialize players
283//! let mut players = BTreeMap::new();
284//! for private_key in &private_keys {
285//!     let player = Player::<MinSig, ed25519::PrivateKey>::new(
286//!         info.clone(),
287//!         private_key.clone(),
288//!     )?;
289//!     players.insert(private_key.public_key(), player);
290//! }
291//!
292//! // Step 3: Run dealer protocol for each participant
293//! let mut logs = Logs::<MinSig, ed25519::PublicKey, N3f1>::new(info.clone());
294//! for dealer_priv in &private_keys {
295//!     // Each dealer generates messages for all players
296//!     let (mut dealer, pub_msg, priv_msgs) = Dealer::start::<N3f1>(
297//!         &mut rng,
298//!         info.clone(),
299//!         dealer_priv.clone(),
300//!         None,  // no previous share for initial DKG
301//!     )?;
302//!
303//!     // Distribute messages to players and collect acknowledgements
304//!     for (player_pk, priv_msg) in priv_msgs {
305//!         if let Some(player) = players.get_mut(&player_pk) {
306//!             if let Some(ack) = player.dealer_message::<N3f1>(
307//!                 dealer_priv.public_key(),
308//!                 pub_msg.clone(),
309//!                 priv_msg,
310//!             )? {
311//!                 dealer.receive_player_ack(player_pk, ack)?;
312//!             }
313//!         }
314//!     }
315//!
316//!     // Finalize dealer and verify log
317//!     let signed_log = dealer.finalize::<N3f1>();
318//!     if let Some((dealer_pk, log)) = signed_log.check(&info) {
319//!         logs.record(dealer_pk, log);
320//!     }
321//! }
322//!
323//! // Step 4: Players finalize to get their shares
324//! let mut player_shares = BTreeMap::new();
325//! for (player_pk, player) in players {
326//!     let (output, share) = player.finalize::<N3f1, ed25519::Batch>(
327//!       &mut rng,
328//!       logs.clone(),
329//!       &commonware_parallel::Sequential,
330//!     )?;
331//!     println!("Player {:?} got share at index {}", player_pk, share.index);
332//!     player_shares.insert(player_pk, share);
333//! }
334//!
335//! // Step 5: Observer can also compute the public output
336//! let observer_output = observe::<MinSig, ed25519::PublicKey, N3f1, ed25519::Batch>(
337//!     &mut rng,
338//!     logs,
339//!     &commonware_parallel::Sequential,
340//! )?;
341//! println!("DKG completed with threshold {}", observer_output.quorum::<N3f1>());
342//! # Ok(())
343//! # }
344//! ```
345//!
346//! For a complete example with resharing, see [commonware-reshare](https://docs.rs/commonware-reshare).
347
348use crate::{
349    BatchVerifier, PublicKey, Secret, Signer,
350    bls12381::primitives::{
351        group::{Private, Scalar, ScalarReadCfg, Share},
352        sharing::{Mode, ModeVersion, Sharing},
353        variant::Variant,
354    },
355    transcript::{Summary, Transcript, Version},
356};
357use commonware_codec::{
358    Encode, EncodeSize, Mode as CodecMode, RangeCfg, Read, ReadExt, Write, mode, modes,
359};
360use commonware_math::{
361    algebra::{Additive, CryptoGroup, Random, Ring as _},
362    poly::{Interpolator, Poly},
363};
364use commonware_parallel::{Sequential, Strategy};
365#[cfg(feature = "arbitrary")]
366use commonware_utils::N3f1;
367use commonware_utils::{
368    Faults, NZU32, Participant, TryCollect,
369    ordered::{Map, Quorum, Set},
370};
371use core::num::NonZeroU32;
372use rand_core::CryptoRng;
373use std::{borrow::Cow, collections::BTreeMap, marker::PhantomData};
374use thiserror::Error;
375
376const NAMESPACE: &[u8] = b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG";
377const SIG_ACK: &[u8] = b"ack";
378const SIG_LOG: &[u8] = b"log";
379const NOISE_PRE_VERIFY: &[u8] = b"pre_verify";
380
381// Feldman satisfies V0's fixed-schema requirements: its application namespace is fixed, and every
382// later packet has a canonical encoding at a fixed position.
383const TRANSCRIPT_VERSION: Version = Version::V0;
384
385/// An error caused by invalid caller input or incomplete local state.
386#[derive(Clone, Debug, Error)]
387pub enum Error {
388    #[error("missing dealer's share from the previous round")]
389    MissingDealerShare,
390    #[error("player is not present in the list of players")]
391    PlayerNotInRound,
392    #[error("dealer {0} is not present in the round")]
393    DealerNotInRound(String),
394    #[error("invalid number of dealers: {0}")]
395    NumDealers(usize),
396    #[error("invalid number of players: {0}")]
397    NumPlayers(usize),
398    /// The previous output is not internally consistent.
399    #[error("invalid previous DKG output")]
400    InvalidPreviousOutput,
401    /// A persisted dealing is invalid or stale relative to the selected dealer log.
402    #[error("invalid persisted dealing from dealer {dealer}")]
403    InvalidPersistedDealing {
404        /// Dealer associated with the invalid persisted dealing.
405        dealer: String,
406    },
407    /// The supplied logs are bound to a different DKG round.
408    #[error("logs are bound to a different dkg round")]
409    MismatchedLogs,
410    /// The player's state is missing a dealing it should have.
411    ///
412    /// This error is emitted when the player is missing dealings that it should
413    /// otherwise have based on the flow of the protocol. This can only happen if
414    /// the code in this module is used in a stateful way, restoring the
415    /// state of the player from saved information. If this state is corrupted
416    /// on disk, or missing, then this error can happen.
417    #[error("missing player's dealing")]
418    MissingPlayerDealing,
419}
420
421/// The reason a configured dealer's signed log proves a protocol fault.
422#[derive(Clone, Debug, Error)]
423pub enum FaultReason {
424    #[error("dealer log contains an acknowledgement signature that does not verify")]
425    InvalidAck,
426    #[error("dealer reveal does not match its commitment")]
427    InvalidReveal,
428    #[error("invalid dealer commitment degree: expected {expected}, got {actual}")]
429    InvalidCommitmentDegree {
430        /// The commitment degree required by the round.
431        expected: u32,
432        /// The commitment degree supplied by the dealer.
433        actual: u32,
434    },
435    #[error("dealer commitment does not match its previous share")]
436    MismatchedReshareCommitment,
437    #[error("dealer log player set does not match the round")]
438    MismatchedLogPlayers,
439    /// The dealer published explicit results containing too many reveals.
440    ///
441    /// A log that safely omits its results with [`DealerLogSummary::TooManyReveals`]
442    /// is unavailable, not faulty.
443    #[error("dealer log publishes too many reveals")]
444    ExcessiveReveals,
445}
446
447/// The reason an initial dealer message was rejected.
448///
449/// Initial dealer messages carry no dealer signature in this construction, so
450/// the caller is responsible for authenticating the `dealer` supplied to
451/// [`Player::dealer_message`]. The dealer later signs the public message as part
452/// of the [`SignedDealerLog`] produced by [`Dealer::finalize`].
453#[derive(Clone, Debug, Error)]
454pub enum DealerMessageError {
455    #[error("participant is not a dealer in this round")]
456    UnexpectedDealer,
457    #[error("invalid dealer commitment degree: expected {expected}, got {actual}")]
458    InvalidCommitmentDegree {
459        /// The commitment degree required by the round.
460        expected: u32,
461        /// The commitment degree supplied by the dealer.
462        actual: u32,
463    },
464    #[error("dealer commitment does not match its previous share")]
465    MismatchedReshareCommitment,
466    #[error("dealer share does not match its commitment")]
467    InvalidDealerShare,
468}
469
470/// The reason a live player acknowledgement was rejected.
471#[derive(Clone, Debug, Error)]
472pub enum PlayerAckError {
473    #[error("participant is not a player in this round")]
474    UnexpectedPlayer,
475    #[error("player acknowledgement signature does not match the dealer transcript")]
476    InvalidAck,
477}
478
479/// Failures determined solely from a dealer's public message.
480///
481/// The shared validator returns exactly these cases. Each validation boundary
482/// maps them into its operation-specific public error without widening that
483/// error's reachable variants.
484enum DealerPubMsgError {
485    /// The commitment degree differs from the degree required by the round.
486    InvalidCommitmentDegree { expected: u32, actual: u32 },
487    /// The commitment constant does not preserve the dealer's previous share.
488    MismatchedReshareCommitment,
489}
490
491// Live validation exposes the rejection without claiming signed-log evidence.
492impl From<DealerPubMsgError> for DealerMessageError {
493    fn from(error: DealerPubMsgError) -> Self {
494        match error {
495            DealerPubMsgError::InvalidCommitmentDegree { expected, actual } => {
496                Self::InvalidCommitmentDegree { expected, actual }
497            }
498            DealerPubMsgError::MismatchedReshareCommitment => Self::MismatchedReshareCommitment,
499        }
500    }
501}
502
503// Signed-log validation records the same rejection as dealer-authenticated evidence.
504impl From<DealerPubMsgError> for FaultReason {
505    fn from(error: DealerPubMsgError) -> Self {
506        match error {
507            DealerPubMsgError::InvalidCommitmentDegree { expected, actual } => {
508                Self::InvalidCommitmentDegree { expected, actual }
509            }
510            DealerPubMsgError::MismatchedReshareCommitment => Self::MismatchedReshareCommitment,
511        }
512    }
513}
514
515/// Non-fault outcome of checking a dealer log against a round.
516#[derive(Clone, Copy, Debug, Eq, PartialEq)]
517enum DealerLogOutcome {
518    /// The configured dealer's log contains verified results.
519    Available,
520    /// The configured dealer omitted results to avoid excessive reveals.
521    Unavailable,
522}
523
524/// The reason a dealer log was rejected for a round.
525#[derive(Clone, Debug)]
526enum DealerLogError {
527    /// The supplied identity is not a dealer in the round.
528    UnexpectedDealer,
529    /// The configured dealer's signed log proves a protocol fault.
530    Fault(FaultReason),
531}
532
533/// A protocol round failure.
534#[derive(Debug, Error)]
535pub enum Failure<P> {
536    /// Too few usable dealer logs remain to complete the DKG.
537    #[error("insufficient usable dealer logs: required={required}, found={found}")]
538    InsufficientLogs {
539        /// Number of usable dealer logs required to complete the DKG.
540        required: u32,
541        /// Number of usable dealer logs found.
542        found: u32,
543        /// Configured dealers whose signed logs prove a protocol fault.
544        ///
545        /// Attribution assumes [`Logs::record`] receives only pairs returned by
546        /// [`SignedDealerLog::check`].
547        faults: Map<P, FaultReason>,
548        /// Configured dealers for which no usable log was available.
549        ///
550        /// This includes missing logs and logs that safely omit their results
551        /// because publishing them would reveal too many shares.
552        unavailable: Set<P>,
553    },
554}
555
556/// An error finalizing a player's DKG output and private share.
557#[derive(Debug, Error)]
558pub enum FinalizeError<P> {
559    /// The caller supplied invalid input or incomplete local state.
560    #[error(transparent)]
561    Error(#[from] Error),
562    /// The protocol round failed.
563    #[error(transparent)]
564    Failure(#[from] Failure<P>),
565}
566
567/// The output of a successful DKG.
568#[derive(Debug, Clone, PartialEq, Eq)]
569pub struct Output<V: Variant, P> {
570    summary: Summary,
571    public: Sharing<V>,
572    dealers: Set<P>,
573    players: Set<P>,
574    revealed: Set<P>,
575}
576
577impl<V: Variant, P: Ord> Output<V, P> {
578    fn share_commitment(&self, player: &P) -> Option<V::Public> {
579        self.public.partial_public(self.players.index(player)?).ok()
580    }
581
582    /// Return the quorum, i.e. the number of players needed to reconstruct the key.
583    pub fn quorum<M: Faults>(&self) -> u32 {
584        self.players.quorum::<M>()
585    }
586
587    /// Get the public polynomial associated with this output.
588    ///
589    /// This is useful for verifying partial signatures, with [crate::bls12381::primitives::ops::threshold::verify_message].
590    pub const fn public(&self) -> &Sharing<V> {
591        &self.public
592    }
593
594    /// Return the dealers who were selected in this round of the DKG.
595    pub const fn dealers(&self) -> &Set<P> {
596        &self.dealers
597    }
598
599    /// Return the players who participated in this round of the DKG, and should have shares.
600    pub const fn players(&self) -> &Set<P> {
601        &self.players
602    }
603
604    /// Return the set of players whose shares may have been revealed.
605    ///
606    /// These are players whose shares can be reconstructed from the selected dealer reveals.
607    pub const fn revealed(&self) -> &Set<P> {
608        &self.revealed
609    }
610}
611
612impl<V: Variant, P: PublicKey> EncodeSize for Output<V, P> {
613    fn encode_size(&self) -> usize {
614        self.summary.encode_size()
615            + self.public.encode_size()
616            + self.dealers.encode_size()
617            + self.players.encode_size()
618            + self.revealed.encode_size()
619    }
620}
621
622impl<V: Variant, P: PublicKey> Write for Output<V, P> {
623    fn write(&self, buf: &mut impl bytes::BufMut) {
624        self.summary.write(buf);
625        self.public.write(buf);
626        self.dealers.write(buf);
627        self.players.write(buf);
628        self.revealed.write(buf);
629    }
630}
631
632impl<V: Variant, P: PublicKey> Read for Output<V, P> {
633    type Cfg = (NonZeroU32, ModeVersion);
634
635    fn read_cfg(
636        buf: &mut impl bytes::Buf,
637        (max_participants, max_supported_mode): &Self::Cfg,
638    ) -> Result<Self, commonware_codec::Error> {
639        let max_participants_usize = max_participants.get() as usize;
640        Ok(Self {
641            summary: ReadExt::read(buf)?,
642            public: Read::read_cfg(buf, &(*max_participants, *max_supported_mode))?,
643            dealers: Read::read_cfg(buf, &(RangeCfg::new(1..=max_participants_usize), ()))?, // at least one dealer must be part of a dealing
644            players: Read::read_cfg(buf, &(RangeCfg::new(1..=max_participants_usize), ()))?, // at least one player must be part of a dealing
645            revealed: Read::read_cfg(buf, &(RangeCfg::new(0..=max_participants_usize), ()))?, // there may not be any reveals
646        })
647    }
648}
649
650#[cfg(feature = "arbitrary")]
651impl<P: PublicKey, V: Variant> arbitrary::Arbitrary<'_> for Output<V, P>
652where
653    P: for<'a> arbitrary::Arbitrary<'a> + Ord,
654    V::Public: for<'a> arbitrary::Arbitrary<'a>,
655{
656    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
657        let summary = u.arbitrary()?;
658        let public: Sharing<V> = u.arbitrary()?;
659        let total = public.total().get() as usize;
660
661        let num_dealers = u.int_in_range(1..=total * 2)?;
662        let dealers = Set::try_from(
663            u.arbitrary_iter::<P>()?
664                .take(num_dealers)
665                .collect::<Result<Vec<_>, _>>()?,
666        )
667        .map_err(|_| arbitrary::Error::IncorrectFormat)?;
668
669        let players = Set::try_from(
670            u.arbitrary_iter::<P>()?
671                .take(total)
672                .collect::<Result<Vec<_>, _>>()?,
673        )
674        .map_err(|_| arbitrary::Error::IncorrectFormat)?;
675
676        let max_revealed = N3f1::max_faults(total) as usize;
677        let revealed = Set::from_iter_dedup(
678            players
679                .iter()
680                .filter(|_| u.arbitrary::<bool>().unwrap_or(false))
681                .take(max_revealed)
682                .cloned(),
683        );
684
685        Ok(Self {
686            summary,
687            public,
688            dealers,
689            players,
690            revealed,
691        })
692    }
693}
694
695/// Revealed-share calculation used by a DKG ceremony.
696#[derive(Clone, Copy, Debug, Eq, PartialEq)]
697#[repr(u8)]
698pub enum Reveal {
699    /// Original calculation based on the number of players in the new sharing.
700    #[deprecated(note = "uses the new player set instead of the contributor set")]
701    V0 = 0,
702    /// Contributor-aware calculation based on the dealers or previous players.
703    V1 = 1,
704}
705
706#[allow(deprecated)]
707impl From<Reveal> for CodecMode {
708    fn from(reveal: Reveal) -> Self {
709        match reveal {
710            Reveal::V0 => mode!(0),
711            Reveal::V1 => mode!(1),
712        }
713    }
714}
715
716/// Information about the current round of the DKG.
717///
718/// This is used to bind signatures to the current round, and to provide the
719/// information that dealers, players, and observers need to perform their actions.
720/// Every operation using an [`Info`] must use the same [`Faults`] implementation
721/// that constructed it. Reshares must retain that fault model across rounds.
722#[derive(Debug, Clone)]
723pub struct Info<V: Variant, P> {
724    summary: Summary,
725    round: u64,
726    previous: Option<Output<V, P>>,
727    mode: Mode,
728    reveal: Reveal,
729    dealers: Set<P>,
730    players: Set<P>,
731}
732
733impl<V: Variant, P: PublicKey> PartialEq for Info<V, P> {
734    fn eq(&self, other: &Self) -> bool {
735        self.summary == other.summary
736    }
737}
738
739impl<V: Variant, P: PublicKey> Info<V, P> {
740    /// Figure out what the dealer share should be.
741    ///
742    /// If there's no previous round, we need a random value, hence `rng`.
743    ///
744    /// However, if there is a previous round, we expect a share, hence `Result`.
745    fn unwrap_or_random_share(
746        &self,
747        mut rng: impl CryptoRng,
748        share: Option<Scalar>,
749    ) -> Result<Scalar, Error> {
750        let out = match (self.previous.as_ref(), share) {
751            (None, None) => Scalar::random(&mut rng),
752            (_, Some(x)) => x,
753            (Some(_), None) => return Err(Error::MissingDealerShare),
754        };
755        Ok(out)
756    }
757
758    const fn num_players(&self) -> NonZeroU32 {
759        // Will not panic because we check that the number of players is non-empty in `new`
760        NZU32!(self.players.len() as u32)
761    }
762
763    fn degree<M: Faults>(&self) -> u32 {
764        self.players.quorum::<M>().saturating_sub(1)
765    }
766
767    fn required_commitments<M: Faults>(&self) -> u32 {
768        let dealer_quorum = self.dealers.quorum::<M>();
769        let prev_quorum = self
770            .previous
771            .as_ref()
772            .map(Output::quorum::<M>)
773            .unwrap_or(u32::MIN);
774        dealer_quorum.max(prev_quorum)
775    }
776
777    fn max_reveals<M: Faults>(&self) -> u32 {
778        self.players.max_faults::<M>()
779    }
780
781    fn reveal_threshold<M: Faults>(&self) -> u32 {
782        #[allow(deprecated)]
783        match self.reveal {
784            Reveal::V0 => self.players.max_faults::<M>() + 1,
785            Reveal::V1 => {
786                let max_faults = self.previous.as_ref().map_or_else(
787                    || self.dealers.max_faults::<M>(),
788                    |previous| previous.players.max_faults::<M>(),
789                );
790                self.required_commitments::<M>()
791                    .checked_sub(max_faults)
792                    .expect("a quorum must contain more participants than max_faults")
793            }
794        }
795    }
796
797    fn player_index(&self, player: &P) -> Option<Participant> {
798        self.players.index(player)
799    }
800
801    fn dealer_index(&self, dealer: &P) -> Option<Participant> {
802        self.dealers.index(dealer)
803    }
804
805    fn player_scalar(&self, player: &P) -> Option<Scalar> {
806        self.mode
807            .scalar(self.num_players(), self.player_index(player)?)
808    }
809
810    fn check_dealer_pub_msg<M: Faults>(
811        &self,
812        dealer: &P,
813        pub_msg: &DealerPubMsg<V>,
814    ) -> Result<(), DealerPubMsgError> {
815        let expected = self.degree::<M>();
816        let actual = pub_msg.commitment.degree_exact();
817        if expected != actual {
818            return Err(DealerPubMsgError::InvalidCommitmentDegree { expected, actual });
819        }
820        if let Some(previous) = self.previous.as_ref() {
821            let share_commitment = previous
822                .share_commitment(dealer)
823                .expect("Info::new validates previous output and dealer membership");
824            if *pub_msg.commitment.constant() != share_commitment {
825                return Err(DealerPubMsgError::MismatchedReshareCommitment);
826            }
827        }
828        Ok(())
829    }
830
831    fn check_dealer_priv_msg(
832        &self,
833        player: Participant,
834        pub_msg: &DealerPubMsg<V>,
835        priv_msg: &DealerPrivMsg,
836    ) -> bool {
837        let scalar = self
838            .mode
839            .scalar(self.num_players(), player)
840            .expect("Player::new validates the participant index");
841        let expected = pub_msg.commitment.eval_msm(&scalar, &Sequential);
842        priv_msg
843            .share
844            .expose(|share| expected == V::Public::generator() * share)
845    }
846
847    fn check_dealer_log<M: Faults, B: BatchVerifier<PublicKey = P>>(
848        &self,
849        rng: &mut impl CryptoRng,
850        strategy: &impl Strategy,
851        round_transcript: &Transcript,
852        dealer: &P,
853        log: &DealerLog<V, P>,
854    ) -> Result<DealerLogOutcome, DealerLogError> {
855        if self.dealer_index(dealer).is_none() {
856            return Err(DealerLogError::UnexpectedDealer);
857        }
858        self.check_dealer_pub_msg::<M>(dealer, &log.pub_msg)
859            .map_err(|error| DealerLogError::Fault(error.into()))?;
860        let Some(results_iter) = log
861            .zip_players(&self.players)
862            .map_err(DealerLogError::Fault)?
863        else {
864            return Ok(DealerLogOutcome::Unavailable);
865        };
866        let ack_summary = transcript_for_ack(round_transcript, dealer, &log.pub_msg).summarize();
867        let mut ack_batch = B::new(self.players.len());
868        let mut reveal_count = 0;
869        let max_reveals = self.max_reveals::<M>();
870        let mut reveal_eval_points = Vec::new();
871        let mut reveal_sum = Scalar::zero();
872        for (player, result) in results_iter {
873            match result {
874                AckOrReveal::Ack(ack) => {
875                    if !ack_summary.add_to_batch(&mut ack_batch, player, &ack.sig) {
876                        return Err(DealerLogError::Fault(FaultReason::InvalidAck));
877                    }
878                }
879                AckOrReveal::Reveal(priv_msg) => {
880                    reveal_count += 1;
881                    if reveal_count > max_reveals {
882                        return Err(DealerLogError::Fault(FaultReason::ExcessiveReveals));
883                    }
884                    let player_scalar = self
885                        .player_scalar(player)
886                        .expect("log players were matched against the round");
887                    let coeff = if reveal_count == 1 {
888                        Scalar::one()
889                    } else {
890                        Scalar::random(&mut *rng)
891                    };
892                    reveal_eval_points.push((coeff.clone(), player_scalar));
893                    priv_msg
894                        .share
895                        .expose(|share| reveal_sum += &(coeff * share));
896                }
897            }
898        }
899        if !ack_batch.verify(&mut *rng, strategy) {
900            return Err(DealerLogError::Fault(FaultReason::InvalidAck));
901        }
902        let lhs = log.pub_msg.commitment.lin_comb_eval(
903            reveal_eval_points
904                .into_iter()
905                .map(|(coeff, point)| (coeff, Cow::Owned(point))),
906            strategy,
907        );
908        if lhs != V::Public::generator() * &reveal_sum {
909            return Err(DealerLogError::Fault(FaultReason::InvalidReveal));
910        }
911        Ok(DealerLogOutcome::Available)
912    }
913}
914
915impl<V: Variant, P: PublicKey> Info<V, P> {
916    /// Create a new [`Info`].
917    ///
918    /// `namespace` must be provided to isolate different applications
919    /// performing DKGs from each other. It must remain fixed across all rounds,
920    /// epochs, restarts, and participants in the protocol's lifetime.
921    /// `round` should be a counter, always incrementing, even for failed DKGs.
922    /// `previous` should be the result of the previous successful DKG. Its public sharing must
923    /// have a participant count matching its player set and an exact recovery threshold equal to
924    /// the fault-model quorum.
925    /// `reveal` selects the revealed-share calculation.
926    /// `dealers` should be the list of public keys for the dealers. This MUST
927    /// be a subset of the previous round's players.
928    /// `players` should be the list of public keys for the players.
929    pub fn new<M: Faults>(
930        namespace: &[u8],
931        round: u64,
932        previous: Option<Output<V, P>>,
933        mode: Mode,
934        reveal: Reveal,
935        dealers: Set<P>,
936        players: Set<P>,
937    ) -> Result<Self, Error> {
938        let participant_range = 1..u32::MAX as usize;
939        if !participant_range.contains(&dealers.len()) {
940            return Err(Error::NumDealers(dealers.len()));
941        }
942        if !participant_range.contains(&players.len()) {
943            return Err(Error::NumPlayers(players.len()));
944        }
945        if let Some(previous) = previous.as_ref() {
946            if Some(previous.public.total().get()) != u32::try_from(previous.players.len()).ok()
947                || previous.public.required() != previous.quorum::<M>()
948            {
949                return Err(Error::InvalidPreviousOutput);
950            }
951            if let Some(unknown) = dealers
952                .iter()
953                .find(|d| previous.players.position(d).is_none())
954            {
955                return Err(Error::DealerNotInRound(format!("{unknown:?}")));
956            }
957            if dealers.len() < previous.quorum::<M>() as usize {
958                return Err(Error::NumDealers(dealers.len()));
959            }
960        }
961        let summary = {
962            let mut transcript = Transcript::new(NAMESPACE, TRANSCRIPT_VERSION);
963            transcript
964                .commit(namespace)
965                .commit(round.encode())
966                .commit(previous.encode())
967                .commit(dealers.encode())
968                .commit(players.encode());
969
970            // Record the selected polynomial evaluation and revealed-share calculations.
971            if let Some(modes) = modes![mode, reveal] {
972                transcript.commit(modes.encode());
973            }
974            transcript.summarize()
975        };
976        Ok(Self {
977            summary,
978            round,
979            previous,
980            mode,
981            reveal,
982            dealers,
983            players,
984        })
985    }
986
987    /// Return the round number for this round.
988    ///
989    /// Round numbers should increase sequentially.
990    pub const fn round(&self) -> u64 {
991        self.round
992    }
993}
994
995#[derive(Clone, Debug)]
996pub struct DealerPubMsg<V: Variant> {
997    commitment: Poly<V::Public>,
998}
999
1000impl<V: Variant> PartialEq for DealerPubMsg<V> {
1001    fn eq(&self, other: &Self) -> bool {
1002        self.commitment == other.commitment
1003    }
1004}
1005
1006impl<V: Variant> Eq for DealerPubMsg<V> {}
1007
1008impl<V: Variant> EncodeSize for DealerPubMsg<V> {
1009    fn encode_size(&self) -> usize {
1010        self.commitment.encode_size()
1011    }
1012}
1013
1014impl<V: Variant> Write for DealerPubMsg<V> {
1015    fn write(&self, buf: &mut impl bytes::BufMut) {
1016        self.commitment.write(buf);
1017    }
1018}
1019
1020impl<V: Variant> Read for DealerPubMsg<V> {
1021    type Cfg = NonZeroU32;
1022
1023    fn read_cfg(
1024        buf: &mut impl bytes::Buf,
1025        &max_size: &Self::Cfg,
1026    ) -> Result<Self, commonware_codec::Error> {
1027        Ok(Self {
1028            commitment: Read::read_cfg(buf, &(RangeCfg::from(NZU32!(1)..=max_size), ()))?,
1029        })
1030    }
1031}
1032
1033#[cfg(feature = "arbitrary")]
1034impl<V: Variant> arbitrary::Arbitrary<'_> for DealerPubMsg<V>
1035where
1036    V::Public: for<'a> arbitrary::Arbitrary<'a>,
1037{
1038    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
1039        let commitment = u.arbitrary()?;
1040        Ok(Self { commitment })
1041    }
1042}
1043
1044#[derive(Clone, Debug, PartialEq, Eq)]
1045pub struct DealerPrivMsg {
1046    share: Secret<Scalar>,
1047}
1048
1049impl DealerPrivMsg {
1050    /// Creates a new `DealerPrivMsg` with the given share.
1051    pub const fn new(share: Scalar) -> Self {
1052        Self {
1053            share: Secret::new(share),
1054        }
1055    }
1056}
1057
1058impl EncodeSize for DealerPrivMsg {
1059    fn encode_size(&self) -> usize {
1060        self.share.expose(|share| share.encode_size())
1061    }
1062}
1063
1064impl Write for DealerPrivMsg {
1065    fn write(&self, buf: &mut impl bytes::BufMut) {
1066        self.share.expose(|share| share.write(buf));
1067    }
1068}
1069
1070impl Read for DealerPrivMsg {
1071    type Cfg = ();
1072
1073    fn read_cfg(
1074        buf: &mut impl bytes::Buf,
1075        _cfg: &Self::Cfg,
1076    ) -> Result<Self, commonware_codec::Error> {
1077        Ok(Self::new(Scalar::read_cfg(
1078            buf,
1079            &ScalarReadCfg::RejectZero,
1080        )?))
1081    }
1082}
1083
1084#[cfg(feature = "arbitrary")]
1085impl arbitrary::Arbitrary<'_> for DealerPrivMsg {
1086    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
1087        Ok(Self::new(u.arbitrary()?))
1088    }
1089}
1090
1091#[derive(Clone, Debug)]
1092pub struct PlayerAck<P: PublicKey> {
1093    sig: P::Signature,
1094}
1095
1096impl<P: PublicKey> PartialEq for PlayerAck<P> {
1097    fn eq(&self, other: &Self) -> bool {
1098        self.sig == other.sig
1099    }
1100}
1101
1102impl<P: PublicKey> EncodeSize for PlayerAck<P> {
1103    fn encode_size(&self) -> usize {
1104        self.sig.encode_size()
1105    }
1106}
1107
1108impl<P: PublicKey> Write for PlayerAck<P> {
1109    fn write(&self, buf: &mut impl bytes::BufMut) {
1110        self.sig.write(buf);
1111    }
1112}
1113
1114impl<P: PublicKey> Read for PlayerAck<P> {
1115    type Cfg = ();
1116
1117    fn read_cfg(
1118        buf: &mut impl bytes::Buf,
1119        _cfg: &Self::Cfg,
1120    ) -> Result<Self, commonware_codec::Error> {
1121        Ok(Self {
1122            sig: ReadExt::read(buf)?,
1123        })
1124    }
1125}
1126
1127#[cfg(feature = "arbitrary")]
1128impl<P: PublicKey> arbitrary::Arbitrary<'_> for PlayerAck<P>
1129where
1130    P::Signature: for<'a> arbitrary::Arbitrary<'a>,
1131{
1132    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
1133        let sig = u.arbitrary()?;
1134        Ok(Self { sig })
1135    }
1136}
1137
1138#[derive(Clone, PartialEq)]
1139enum AckOrReveal<P: PublicKey> {
1140    Ack(PlayerAck<P>),
1141    Reveal(DealerPrivMsg),
1142}
1143
1144impl<P: PublicKey> AckOrReveal<P> {
1145    const fn is_reveal(&self) -> bool {
1146        matches!(*self, Self::Reveal(_))
1147    }
1148}
1149
1150impl<P: PublicKey> std::fmt::Debug for AckOrReveal<P> {
1151    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
1152        match self {
1153            Self::Ack(x) => write!(f, "Ack({x:?})"),
1154            Self::Reveal(_) => write!(f, "Reveal(REDACTED)"),
1155        }
1156    }
1157}
1158
1159impl<P: PublicKey> EncodeSize for AckOrReveal<P> {
1160    fn encode_size(&self) -> usize {
1161        1 + match self {
1162            Self::Ack(x) => x.encode_size(),
1163            Self::Reveal(x) => x.encode_size(),
1164        }
1165    }
1166}
1167
1168impl<P: PublicKey> Write for AckOrReveal<P> {
1169    fn write(&self, buf: &mut impl bytes::BufMut) {
1170        match self {
1171            Self::Ack(x) => {
1172                0u8.write(buf);
1173                x.write(buf);
1174            }
1175            Self::Reveal(x) => {
1176                1u8.write(buf);
1177                x.write(buf);
1178            }
1179        }
1180    }
1181}
1182
1183impl<P: PublicKey> Read for AckOrReveal<P> {
1184    type Cfg = ();
1185
1186    fn read_cfg(
1187        buf: &mut impl bytes::Buf,
1188        _cfg: &Self::Cfg,
1189    ) -> Result<Self, commonware_codec::Error> {
1190        let tag = u8::read(buf)?;
1191        match tag {
1192            0 => Ok(Self::Ack(ReadExt::read(buf)?)),
1193            1 => Ok(Self::Reveal(ReadExt::read(buf)?)),
1194            x => Err(commonware_codec::Error::InvalidEnum(x)),
1195        }
1196    }
1197}
1198
1199#[cfg(feature = "arbitrary")]
1200impl<P: PublicKey> arbitrary::Arbitrary<'_> for AckOrReveal<P>
1201where
1202    P: for<'a> arbitrary::Arbitrary<'a>,
1203    P::Signature: for<'a> arbitrary::Arbitrary<'a>,
1204{
1205    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
1206        let choice = u.int_in_range(0..=1)?;
1207        match choice {
1208            0 => {
1209                let ack = u.arbitrary()?;
1210                Ok(Self::Ack(ack))
1211            }
1212            1 => {
1213                let reveal = u.arbitrary()?;
1214                Ok(Self::Reveal(reveal))
1215            }
1216            _ => unreachable!(),
1217        }
1218    }
1219}
1220
1221#[derive(Clone, Debug)]
1222enum DealerResult<P: PublicKey> {
1223    Ok(Map<P, AckOrReveal<P>>),
1224    TooManyReveals,
1225}
1226
1227impl<P: PublicKey> PartialEq for DealerResult<P> {
1228    fn eq(&self, other: &Self) -> bool {
1229        match (self, other) {
1230            (Self::Ok(x), Self::Ok(y)) => x == y,
1231            (Self::TooManyReveals, Self::TooManyReveals) => true,
1232            _ => false,
1233        }
1234    }
1235}
1236
1237impl<P: PublicKey> EncodeSize for DealerResult<P> {
1238    fn encode_size(&self) -> usize {
1239        1 + match self {
1240            Self::Ok(r) => r.encode_size(),
1241            Self::TooManyReveals => 0,
1242        }
1243    }
1244}
1245
1246impl<P: PublicKey> Write for DealerResult<P> {
1247    fn write(&self, buf: &mut impl bytes::BufMut) {
1248        match self {
1249            Self::Ok(r) => {
1250                0u8.write(buf);
1251                r.write(buf);
1252            }
1253            Self::TooManyReveals => {
1254                1u8.write(buf);
1255            }
1256        }
1257    }
1258}
1259
1260impl<P: PublicKey> Read for DealerResult<P> {
1261    type Cfg = NonZeroU32;
1262
1263    fn read_cfg(
1264        buf: &mut impl bytes::Buf,
1265        &max_players: &Self::Cfg,
1266    ) -> Result<Self, commonware_codec::Error> {
1267        let tag = u8::read(buf)?;
1268        match tag {
1269            0 => Ok(Self::Ok(Read::read_cfg(
1270                buf,
1271                &(RangeCfg::from(0..=max_players.get() as usize), (), ()),
1272            )?)),
1273            1 => Ok(Self::TooManyReveals),
1274            x => Err(commonware_codec::Error::InvalidEnum(x)),
1275        }
1276    }
1277}
1278
1279#[cfg(feature = "arbitrary")]
1280impl<P: PublicKey> arbitrary::Arbitrary<'_> for DealerResult<P>
1281where
1282    P: for<'a> arbitrary::Arbitrary<'a>,
1283    P::Signature: for<'a> arbitrary::Arbitrary<'a>,
1284{
1285    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
1286        let choice = u.int_in_range(0..=1)?;
1287        match choice {
1288            0 => {
1289                use commonware_utils::TryFromIterator;
1290                use std::collections::HashMap;
1291
1292                let base: HashMap<P, AckOrReveal<P>> = u.arbitrary()?;
1293                let map =
1294                    Map::try_from_iter(base).map_err(|_| arbitrary::Error::IncorrectFormat)?;
1295
1296                Ok(Self::Ok(map))
1297            }
1298            1 => Ok(Self::TooManyReveals),
1299            _ => unreachable!(),
1300        }
1301    }
1302}
1303
1304#[derive(Clone, Debug)]
1305pub struct DealerLog<V: Variant, P: PublicKey> {
1306    pub_msg: DealerPubMsg<V>,
1307    results: DealerResult<P>,
1308}
1309
1310impl<V: Variant, P: PublicKey> PartialEq for DealerLog<V, P> {
1311    fn eq(&self, other: &Self) -> bool {
1312        self.pub_msg == other.pub_msg && self.results == other.results
1313    }
1314}
1315
1316impl<V: Variant, P: PublicKey> EncodeSize for DealerLog<V, P> {
1317    fn encode_size(&self) -> usize {
1318        self.pub_msg.encode_size() + self.results.encode_size()
1319    }
1320}
1321
1322impl<V: Variant, P: PublicKey> Write for DealerLog<V, P> {
1323    fn write(&self, buf: &mut impl bytes::BufMut) {
1324        self.pub_msg.write(buf);
1325        self.results.write(buf);
1326    }
1327}
1328
1329impl<V: Variant, P: PublicKey> Read for DealerLog<V, P> {
1330    type Cfg = NonZeroU32;
1331
1332    fn read_cfg(
1333        buf: &mut impl bytes::Buf,
1334        cfg: &Self::Cfg,
1335    ) -> Result<Self, commonware_codec::Error> {
1336        Ok(Self {
1337            pub_msg: Read::read_cfg(buf, cfg)?,
1338            results: Read::read_cfg(buf, cfg)?,
1339        })
1340    }
1341}
1342
1343impl<V: Variant, P: PublicKey> DealerLog<V, P> {
1344    fn get_ack(&self, player: &P) -> Option<&PlayerAck<P>> {
1345        let DealerResult::Ok(results) = &self.results else {
1346            return None;
1347        };
1348        match results.get_value(player) {
1349            Some(AckOrReveal::Ack(ack)) => Some(ack),
1350            _ => None,
1351        }
1352    }
1353
1354    fn get_reveal(&self, player: &P) -> Option<&DealerPrivMsg> {
1355        let DealerResult::Ok(results) = &self.results else {
1356            return None;
1357        };
1358        match results.get_value(player) {
1359            Some(AckOrReveal::Reveal(priv_msg)) => Some(priv_msg),
1360            _ => None,
1361        }
1362    }
1363
1364    fn zip_players<'a, 'b>(
1365        &'a self,
1366        players: &'b Set<P>,
1367    ) -> Result<Option<impl Iterator<Item = (&'b P, &'a AckOrReveal<P>)>>, FaultReason> {
1368        match &self.results {
1369            DealerResult::TooManyReveals => Ok(None),
1370            DealerResult::Ok(results) => {
1371                // We don't check this on deserialization.
1372                if results.keys() != players {
1373                    return Err(FaultReason::MismatchedLogPlayers);
1374                }
1375                Ok(Some(players.iter().zip(results.values().iter())))
1376            }
1377        }
1378    }
1379
1380    /// Return a [`DealerLogSummary`] of the results in this log.
1381    ///
1382    /// This can be useful for observing the progress of the DKG.
1383    pub fn summary(&self) -> DealerLogSummary<P> {
1384        match &self.results {
1385            DealerResult::TooManyReveals => DealerLogSummary::TooManyReveals,
1386            DealerResult::Ok(map) => {
1387                let (reveals, acks): (Vec<_>, Vec<_>) =
1388                    map.iter_pairs().partition(|(_, a_r)| a_r.is_reveal());
1389                DealerLogSummary::Ok {
1390                    acks: acks
1391                        .into_iter()
1392                        .map(|(p, _)| p.clone())
1393                        .try_collect()
1394                        .expect("map keys are deduped"),
1395                    reveals: reveals
1396                        .into_iter()
1397                        .map(|(p, _)| p.clone())
1398                        .try_collect()
1399                        .expect("map keys are deduped"),
1400                }
1401            }
1402        }
1403    }
1404}
1405
1406/// Information about the reveals and acks in a [`DealerLog`].
1407// This exists to have a public interface we're happy maintaining, not leaking
1408// internal details about various things.
1409#[derive(Clone, Debug)]
1410pub enum DealerLogSummary<P> {
1411    /// The dealer is refusing to post any information, because they would have
1412    /// too many reveals otherwise.
1413    TooManyReveals,
1414    /// The dealer has some players who acked, and some players who didn't, that it's revealing.
1415    Ok { acks: Set<P>, reveals: Set<P> },
1416}
1417
1418#[cfg(feature = "arbitrary")]
1419impl<V: Variant, P: PublicKey> arbitrary::Arbitrary<'_> for DealerLog<V, P>
1420where
1421    P: for<'a> arbitrary::Arbitrary<'a>,
1422    V::Public: for<'a> arbitrary::Arbitrary<'a>,
1423    P::Signature: for<'a> arbitrary::Arbitrary<'a>,
1424{
1425    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
1426        let pub_msg = u.arbitrary()?;
1427        let results = u.arbitrary()?;
1428        Ok(Self { pub_msg, results })
1429    }
1430}
1431
1432/// A [`DealerLog`], but identified to and signed by a dealer.
1433///
1434/// The [`SignedDealerLog::check`] method allows extracting a public key (the dealer)
1435/// and a [`DealerLog`] from this struct.
1436///
1437/// This avoids having to trust some other party or process for knowing that a
1438/// dealer actually produced a log.
1439#[derive(Clone, Debug)]
1440pub struct SignedDealerLog<V: Variant, S: Signer> {
1441    dealer: S::PublicKey,
1442    log: DealerLog<V, S::PublicKey>,
1443    sig: S::Signature,
1444}
1445
1446impl<V: Variant, S: Signer> PartialEq for SignedDealerLog<V, S> {
1447    fn eq(&self, other: &Self) -> bool {
1448        self.dealer == other.dealer && self.log == other.log && self.sig == other.sig
1449    }
1450}
1451
1452impl<V: Variant, S: Signer> SignedDealerLog<V, S> {
1453    fn sign(sk: &S, info: &Info<V, S::PublicKey>, log: DealerLog<V, S::PublicKey>) -> Self {
1454        let sig = transcript_for_log(info, &log).sign(sk);
1455        Self {
1456            dealer: sk.public_key(),
1457            log,
1458            sig,
1459        }
1460    }
1461
1462    /// Check this log for a particular round.
1463    ///
1464    /// This will produce the public key of the dealer that signed this log,
1465    /// and the underlying log that they signed.
1466    ///
1467    /// This will return [`Option::None`] if the check fails.
1468    #[allow(clippy::type_complexity)]
1469    pub fn check(
1470        self,
1471        info: &Info<V, S::PublicKey>,
1472    ) -> Option<(S::PublicKey, DealerLog<V, S::PublicKey>)> {
1473        if !transcript_for_log(info, &self.log).verify(&self.dealer, &self.sig) {
1474            return None;
1475        }
1476        Some((self.dealer, self.log))
1477    }
1478}
1479
1480impl<V: Variant, S: Signer> EncodeSize for SignedDealerLog<V, S> {
1481    fn encode_size(&self) -> usize {
1482        self.dealer.encode_size() + self.log.encode_size() + self.sig.encode_size()
1483    }
1484}
1485
1486impl<V: Variant, S: Signer> Write for SignedDealerLog<V, S> {
1487    fn write(&self, buf: &mut impl bytes::BufMut) {
1488        self.dealer.write(buf);
1489        self.log.write(buf);
1490        self.sig.write(buf);
1491    }
1492}
1493
1494impl<V: Variant, S: Signer> Read for SignedDealerLog<V, S> {
1495    type Cfg = NonZeroU32;
1496
1497    fn read_cfg(
1498        buf: &mut impl bytes::Buf,
1499        cfg: &Self::Cfg,
1500    ) -> Result<Self, commonware_codec::Error> {
1501        Ok(Self {
1502            dealer: ReadExt::read(buf)?,
1503            log: Read::read_cfg(buf, cfg)?,
1504            sig: ReadExt::read(buf)?,
1505        })
1506    }
1507}
1508
1509#[cfg(feature = "arbitrary")]
1510impl<V: Variant, S: Signer> arbitrary::Arbitrary<'_> for SignedDealerLog<V, S>
1511where
1512    S::PublicKey: for<'a> arbitrary::Arbitrary<'a>,
1513    V::Public: for<'a> arbitrary::Arbitrary<'a>,
1514    S::Signature: for<'a> arbitrary::Arbitrary<'a>,
1515{
1516    fn arbitrary(u: &mut arbitrary::Unstructured<'_>) -> arbitrary::Result<Self> {
1517        let dealer = u.arbitrary()?;
1518        let log = u.arbitrary()?;
1519        let sig = u.arbitrary()?;
1520        Ok(Self { dealer, log, sig })
1521    }
1522}
1523
1524fn transcript_for_round<V: Variant, P: PublicKey>(info: &Info<V, P>) -> Transcript {
1525    Transcript::resume(info.summary, TRANSCRIPT_VERSION)
1526}
1527
1528fn transcript_for_ack<V: Variant, P: PublicKey>(
1529    transcript: &Transcript,
1530    dealer: &P,
1531    pub_msg: &DealerPubMsg<V>,
1532) -> Transcript {
1533    let mut out = transcript.fork(SIG_ACK);
1534    out.commit(dealer.encode());
1535    out.commit(pub_msg.encode());
1536    out
1537}
1538
1539fn transcript_for_log<V: Variant, P: PublicKey>(
1540    info: &Info<V, P>,
1541    log: &DealerLog<V, P>,
1542) -> Transcript {
1543    let mut out = transcript_for_round(info).fork(SIG_LOG);
1544    out.commit(log.encode());
1545    out
1546}
1547
1548/// Accumulates dealer logs for a DKG round and caches verification results so
1549/// `pre_verify` work can be reused until a dealer's log is replaced.
1550#[derive(Clone)]
1551pub struct Logs<V: Variant, P: PublicKey, M: Faults> {
1552    info: Info<V, P>,
1553    logs: BTreeMap<P, DealerLog<V, P>>,
1554    known: BTreeMap<P, Result<DealerLogOutcome, DealerLogError>>,
1555    phantom_m: PhantomData<M>,
1556}
1557
1558// Selected logs stay paired with the round information against which they were validated.
1559type SelectedLogs<V, P> = (Info<V, P>, Map<P, DealerLog<V, P>>);
1560
1561impl<V: Variant, P: PublicKey, M: Faults> Logs<V, P, M> {
1562    /// Create a log set bound to a particular DKG round.
1563    pub fn new(info: Info<V, P>) -> Self {
1564        Self {
1565            info,
1566            logs: Default::default(),
1567            known: Default::default(),
1568            phantom_m: Default::default(),
1569        }
1570    }
1571
1572    fn check_dealers<B: BatchVerifier<PublicKey = P>>(
1573        rng: &mut impl CryptoRng,
1574        info: &Info<V, P>,
1575        strategy: &impl Strategy,
1576        transcript: &Transcript,
1577        dealers: &[(&P, &DealerLog<V, P>)],
1578    ) -> Vec<(P, Result<DealerLogOutcome, DealerLogError>)> {
1579        let checks: Vec<_> = dealers
1580            .iter()
1581            .map(|&(dealer, log)| {
1582                let seed = Summary::random(&mut *rng);
1583                ((*dealer).clone(), log, seed)
1584            })
1585            .collect();
1586
1587        // This uses signature batch verification only for a particular dealer's
1588        // signatures. We could batch across all dealers, but in practice this starts
1589        // performing worse than just using parallelism with at least 4 threads,
1590        // and also introduces a slow path if any dealer has a bad sig. This slow
1591        // path can easily be exercised by an adversary.
1592        strategy.map_collect_vec(checks, |(dealer, log, seed)| {
1593            let mut local_rng =
1594                Transcript::resume(seed, TRANSCRIPT_VERSION).noise(NOISE_PRE_VERIFY);
1595            let result =
1596                info.check_dealer_log::<M, B>(&mut local_rng, strategy, transcript, &dealer, log);
1597            (dealer, result)
1598        })
1599    }
1600
1601    /// Record the log for a particular dealer.
1602    ///
1603    /// Return `true` if the dealer was already present in the log, in which
1604    /// case its log will be replaced.
1605    ///
1606    /// This method does not authenticate the log. Faults reported by
1607    /// [`Failure::InsufficientLogs`] are attributable only when `dealer` and
1608    /// `log` are the pair returned by [`SignedDealerLog::check`].
1609    pub fn record(&mut self, dealer: P, log: DealerLog<V, P>) -> bool {
1610        self.known.remove(&dealer);
1611        self.logs.insert(dealer, log).is_some()
1612    }
1613
1614    /// Verify the logs that we've received so far.
1615    ///
1616    /// This makes finalization faster, by doing some of
1617    /// the verification work now.
1618    ///
1619    /// This method can amortize work over a batch of items. It's more efficient
1620    /// to call it after several [`Self::record`], rather than after
1621    /// each call.
1622    pub fn pre_verify<B: BatchVerifier<PublicKey = P>>(
1623        &mut self,
1624        rng: &mut impl CryptoRng,
1625        strategy: &impl Strategy,
1626    ) {
1627        let required_commitments = self.info.required_commitments::<M>() as usize;
1628        let transcript = transcript_for_round(&self.info);
1629
1630        // Create a pending batch, which we try and optimistically size as small as
1631        // possible, to avoid verifying more dealers than we need, if they're all
1632        // honest.
1633        let mut need = required_commitments;
1634        let mut pending = Vec::new();
1635        let mut iter = self.logs.iter();
1636        while need > 0 {
1637            let Some((dealer, log)) = iter.next() else {
1638                break;
1639            };
1640            match self.known.get(dealer) {
1641                Some(Ok(DealerLogOutcome::Available)) => need -= 1,
1642                Some(_) => {}
1643                None => {
1644                    need -= 1;
1645                    pending.push((dealer, log));
1646                }
1647            }
1648        }
1649
1650        // Verify the batch and update the known usable dealers.
1651        let pending_results =
1652            Self::check_dealers::<B>(rng, &self.info, strategy, &transcript, &pending);
1653        let mut all_pending_usable = true;
1654        for (dealer, result) in pending_results {
1655            let is_usable = matches!(result, Ok(DealerLogOutcome::Available));
1656            self.known.insert(dealer, result);
1657            all_pending_usable &= is_usable;
1658        }
1659        if all_pending_usable {
1660            return;
1661        }
1662
1663        // We could jump back to the start of the function to recalculate the minimal pending set,
1664        // hoping that they would all be valid again. However, in this case, we're
1665        // dealing with some dealers that are malicious, and they might be trying to
1666        // slow down verification as much as possible by making us waste our time with
1667        // undue optimism. We instead adopt a pessimistic approach, assuming the
1668        // worst: that we might need to check all of the remaining dealers
1669        // to find the honest ones we need.
1670        let remaining: Vec<_> = iter
1671            .filter(|(dealer, _)| !self.known.contains_key(*dealer))
1672            .collect();
1673        let results = Self::check_dealers::<B>(rng, &self.info, strategy, &transcript, &remaining);
1674        for (dealer, result) in results {
1675            self.known.insert(dealer, result);
1676        }
1677    }
1678
1679    /// Given the logs we've received, determine which dealer logs to use, if any.
1680    ///
1681    /// This might return an error if there are not enough good logs that we can use.
1682    fn select<B: BatchVerifier<PublicKey = P>>(
1683        mut self,
1684        rng: &mut impl CryptoRng,
1685        strategy: &impl Strategy,
1686    ) -> Result<SelectedLogs<V, P>, Failure<P>> {
1687        self.pre_verify::<B>(rng, strategy);
1688        let required = self.info.required_commitments::<M>();
1689        let required_count =
1690            usize::try_from(required).expect("required commitments exceed usize::MAX");
1691        let out: Map<_, _> = self
1692            .logs
1693            .into_iter()
1694            .filter(|(dealer, _)| {
1695                matches!(
1696                    self.known.get(dealer),
1697                    Some(Ok(DealerLogOutcome::Available))
1698                )
1699            })
1700            .take(required_count)
1701            .try_collect()
1702            .expect("dealers should be unique");
1703        let found = u32::try_from(out.len()).expect("valid dealer count exceeds u32::MAX");
1704        if found < required {
1705            let unavailable =
1706                self.info
1707                    .dealers
1708                    .iter()
1709                    .filter(|dealer| match self.known.get(*dealer) {
1710                        None
1711                        | Some(Ok(DealerLogOutcome::Unavailable))
1712                        | Some(Err(DealerLogError::UnexpectedDealer)) => true,
1713                        Some(Ok(DealerLogOutcome::Available))
1714                        | Some(Err(DealerLogError::Fault(_))) => false,
1715                    })
1716                    .cloned()
1717                    .try_collect::<Set<_>>()
1718                    .expect("configured dealers are unique");
1719            let faults = self
1720                .known
1721                .into_iter()
1722                .filter_map(|(dealer, result)| match result {
1723                    Err(DealerLogError::Fault(fault)) => Some((dealer, fault)),
1724                    Ok(DealerLogOutcome::Available | DealerLogOutcome::Unavailable)
1725                    | Err(DealerLogError::UnexpectedDealer) => None,
1726                })
1727                .try_collect::<Map<_, _>>()
1728                .expect("checked dealers are unique");
1729            return Err(Failure::InsufficientLogs {
1730                required,
1731                found,
1732                faults,
1733                unavailable,
1734            });
1735        }
1736        Ok((self.info, out))
1737    }
1738}
1739
1740pub struct Dealer<V: Variant, S: Signer> {
1741    me: S,
1742    info: Info<V, S::PublicKey>,
1743    pub_msg: DealerPubMsg<V>,
1744    results: Map<S::PublicKey, AckOrReveal<S::PublicKey>>,
1745    transcript: Transcript,
1746}
1747
1748impl<V: Variant, S: Signer> Dealer<V, S> {
1749    /// Create a [`Dealer`].
1750    ///
1751    /// This needs randomness, to generate a dealing.
1752    ///
1753    /// We also need the dealer's private key, in order to produce the [`SignedDealerLog`].
1754    ///
1755    /// If we're doing a reshare, the dealer should have a share from the previous round.
1756    ///
1757    /// This will produce the [`Dealer`], a [`DealerPubMsg`] to send to every player,
1758    /// and a list of [`DealerPrivMsg`]s, along with which players those need to
1759    /// be sent to.
1760    ///
1761    /// The public message can be sent in the clear, but it's important that players
1762    /// know which dealer sent what public message. You MUST ensure that dealers
1763    /// cannot impersonate each-other when sending this message.
1764    ///
1765    /// The private message MUST be sent encrypted (or, in some other way, privately)
1766    /// to the target player. Similarly, that player MUST be convinced that this dealer
1767    /// sent it that message, without any possibility of impersonation. A simple way
1768    /// to provide both guarantees is through an authenticated channel, e.g. via
1769    /// [crate::handshake], or [commonware-p2p](https://docs.rs/commonware-p2p/latest/commonware_p2p/).
1770    #[allow(clippy::type_complexity)]
1771    pub fn start<M: Faults>(
1772        mut rng: impl CryptoRng,
1773        info: Info<V, S::PublicKey>,
1774        me: S,
1775        share: Option<Share>,
1776    ) -> Result<(Self, DealerPubMsg<V>, Vec<(S::PublicKey, DealerPrivMsg)>), Error> {
1777        // Check that this dealer is defined in the round.
1778        info.dealer_index(&me.public_key())
1779            .ok_or_else(|| Error::DealerNotInRound(format!("{:?}", me.public_key())))?;
1780        let share = info.unwrap_or_random_share(
1781            &mut rng,
1782            // We are extracting the private scalar from `Secret` protection because
1783            // `Poly::new_with_constant` requires an owned value. The extracted scalar is
1784            // scoped to this function and will be zeroized on drop (i.e. the secret is
1785            // only exposed for the duration of this function).
1786            share.map(|x| x.private.expose_unwrap()),
1787        )?;
1788        let my_poly = Poly::new_with_constant(&mut rng, info.degree::<M>(), share);
1789        let priv_msgs = info
1790            .players
1791            .iter()
1792            .map(|pk| {
1793                (
1794                    pk.clone(),
1795                    DealerPrivMsg::new(my_poly.eval_msm(
1796                        &info.player_scalar(pk).expect("player should exist"),
1797                        &Sequential,
1798                    )),
1799                )
1800            })
1801            .collect::<Vec<_>>();
1802        let results: Map<_, _> = priv_msgs
1803            .clone()
1804            .into_iter()
1805            .map(|(pk, priv_msg)| (pk, AckOrReveal::Reveal(priv_msg)))
1806            .try_collect()
1807            .expect("players are unique");
1808        let commitment = Poly::commit(my_poly);
1809        let pub_msg = DealerPubMsg { commitment };
1810        let transcript = {
1811            let t = transcript_for_round(&info);
1812            transcript_for_ack(&t, &me.public_key(), &pub_msg)
1813        };
1814        let this = Self {
1815            me,
1816            info,
1817            pub_msg: pub_msg.clone(),
1818            results,
1819            transcript,
1820        };
1821        Ok((this, pub_msg, priv_msgs))
1822    }
1823
1824    /// Process an acknowledgement from a player.
1825    ///
1826    /// Acknowledgements should really only be processed once per player,
1827    /// but this method is idempotent nonetheless.
1828    ///
1829    /// A rejection is not attributed to `player`: a signature mismatch can also
1830    /// arise when a dealer equivocates and the player acknowledges a different
1831    /// public message.
1832    pub fn receive_player_ack(
1833        &mut self,
1834        player: S::PublicKey,
1835        ack: PlayerAck<S::PublicKey>,
1836    ) -> Result<(), PlayerAckError> {
1837        let Some(res_mut) = self.results.get_value_mut(&player) else {
1838            return Err(PlayerAckError::UnexpectedPlayer);
1839        };
1840        if !self.transcript.verify(&player, &ack.sig) {
1841            return Err(PlayerAckError::InvalidAck);
1842        }
1843        *res_mut = AckOrReveal::Ack(ack);
1844        Ok(())
1845    }
1846
1847    /// Finalize the dealer, producing a signed log.
1848    ///
1849    /// This should be called at the point where no more acks will be processed.
1850    pub fn finalize<M: Faults>(self) -> SignedDealerLog<V, S> {
1851        let reveals = self
1852            .results
1853            .values()
1854            .iter()
1855            .filter(|x| x.is_reveal())
1856            .count() as u32;
1857        // Omit results if there are too many reveals.
1858        let results = if reveals > self.info.max_reveals::<M>() {
1859            DealerResult::TooManyReveals
1860        } else {
1861            DealerResult::Ok(self.results)
1862        };
1863        let log = DealerLog {
1864            pub_msg: self.pub_msg,
1865            results,
1866        };
1867        SignedDealerLog::sign(&self.me, &self.info, log)
1868    }
1869}
1870
1871struct Observe<V: Variant, P: PublicKey> {
1872    output: Output<V, P>,
1873    weights: Option<Interpolator<P, Scalar>>,
1874}
1875
1876impl<V: Variant, P: PublicKey> Observe<V, P> {
1877    fn reckon<M: Faults>(
1878        info: Info<V, P>,
1879        selected: Map<P, DealerLog<V, P>>,
1880        strategy: &impl Strategy,
1881    ) -> Self {
1882        // Logs::select validated the shape of each selected log. Track player
1883        // shares reconstructible from their dealer reveals.
1884        let reveal_threshold = info.reveal_threshold::<M>();
1885        let mut reveal_counts: BTreeMap<P, u32> = BTreeMap::new();
1886        let mut revealed = Vec::new();
1887        for log in selected.values() {
1888            let iter = log
1889                .zip_players(&info.players)
1890                .expect("selected dealer log should match the round's players")
1891                .expect("selected dealer log should contain usable results");
1892            for (player, result) in iter {
1893                if !result.is_reveal() {
1894                    continue;
1895                }
1896                let count = reveal_counts.entry(player.clone()).or_insert(0);
1897                *count += 1;
1898                if *count == reveal_threshold {
1899                    revealed.push(player.clone());
1900                }
1901            }
1902        }
1903        let revealed: Set<P> = revealed
1904            .into_iter()
1905            .try_collect()
1906            .expect("players are unique");
1907
1908        // Extract dealers before consuming selected
1909        let dealers: Set<P> = selected
1910            .keys()
1911            .iter()
1912            .cloned()
1913            .try_collect()
1914            .expect("selected dealers are unique");
1915
1916        // Recover the public polynomial
1917        let (public, weights) = if let Some(previous) = info.previous.as_ref() {
1918            let weights = previous
1919                .public()
1920                .mode()
1921                .subset_interpolator(previous.players(), selected.keys())
1922                .expect("the result of select should produce a valid subset");
1923            let commitments = selected
1924                .into_iter()
1925                .map(|(dealer, log)| (dealer, log.pub_msg.commitment))
1926                .try_collect::<Map<_, _>>()
1927                .expect("Map should have unique keys");
1928            let public = weights
1929                .interpolate(&commitments, strategy)
1930                .expect("select checks that enough points have been provided");
1931
1932            // Public-message validation binds each commitment's constant term to the dealer's
1933            // previous public share, so interpolating a valid subset must preserve the public key.
1934            assert_eq!(
1935                previous.public().public(),
1936                public.constant(),
1937                "selected reshare commitments must preserve the previous public key",
1938            );
1939            (public, Some(weights))
1940        } else {
1941            let mut public = Poly::zero();
1942            for log in selected.values() {
1943                public += &log.pub_msg.commitment;
1944            }
1945            (public, None)
1946        };
1947        let n = info.players.len() as u32;
1948        let output = Output {
1949            summary: info.summary,
1950            public: Sharing::new(info.mode, NZU32!(n), public),
1951            dealers,
1952            players: info.players,
1953            revealed,
1954        };
1955        Self { output, weights }
1956    }
1957}
1958
1959/// Observe the result of a DKG, using the public results.
1960///
1961/// The log mapping dealers to their log is the shared piece of information
1962/// that the participants (players, observers) of the DKG must all agree on.
1963///
1964/// From this log, we can (potentially, as the DKG can fail) compute the public output.
1965///
1966/// Returns [`Failure::InsufficientLogs`] if too few usable dealer logs remain.
1967pub fn observe<V: Variant, P: PublicKey, M: Faults, B: BatchVerifier<PublicKey = P>>(
1968    rng: &mut impl CryptoRng,
1969    logs: Logs<V, P, M>,
1970    strategy: &impl Strategy,
1971) -> Result<Output<V, P>, Failure<P>> {
1972    let (info, selected) = logs.select::<B>(rng, strategy)?;
1973    Ok(Observe::<V, P>::reckon::<M>(info, selected, strategy).output)
1974}
1975
1976/// Represents a player in the DKG / reshare process.
1977///
1978/// The player is attempting to get a share of the key.
1979///
1980/// They need not have participated in prior rounds.
1981pub struct Player<V: Variant, S: Signer> {
1982    me: S,
1983    me_pub: S::PublicKey,
1984    info: Info<V, S::PublicKey>,
1985    index: Participant,
1986    transcript: Transcript,
1987    view: BTreeMap<S::PublicKey, (DealerPubMsg<V>, DealerPrivMsg)>,
1988}
1989
1990impl<V: Variant, S: Signer> Player<V, S> {
1991    /// Create a new [`Player`].
1992    ///
1993    /// We need the player's private key in order to sign messages.
1994    pub fn new(info: Info<V, S::PublicKey>, me: S) -> Result<Self, Error> {
1995        let me_pub = me.public_key();
1996        Ok(Self {
1997            index: info.player_index(&me_pub).ok_or(Error::PlayerNotInRound)?,
1998            me,
1999            me_pub,
2000            transcript: transcript_for_round(&info),
2001            info,
2002            view: BTreeMap::new(),
2003        })
2004    }
2005
2006    /// Resume a [`Player`], given some existing public state.
2007    ///
2008    /// This is equivalent to calling [`Self::new`] and then [`Self::dealer_message`]
2009    /// with the appropriate messages, but includes extra safeguards to detect
2010    /// missing / corrupted state.
2011    ///
2012    /// It's imperative that the `logs` passed in have been verified. This is done
2013    /// naturally when converting from a [`SignedDealerLog`] to a [`DealerLog`],
2014    /// but this function, like [`Player::finalize`], assumes that this check has
2015    /// been done.
2016    ///
2017    /// All messages the player should have received must be passed into this method,
2018    /// and if any messages which should be present based on this player's actions
2019    /// in the log are missing, this method will return [`Error::MissingPlayerDealing`].
2020    ///
2021    /// For example, if no private message from a dealer is present in `msgs`, but
2022    /// we've already acknowledged one, and this has been included in a public log,
2023    /// then this method will fail.
2024    ///
2025    /// This method cannot catch all cases where state has been corrupted. In
2026    /// particular, if a dealer has not posted their log publicly yet, but has
2027    /// already received an ack, then this method cannot help in that case,
2028    /// but the issue still remains.
2029    ///
2030    /// The returned map contains the acknowledgements generated while replaying
2031    /// `msgs`, keyed by dealer. Invalid replayed messages return
2032    /// [`Error::InvalidPersistedDealing`], because `msgs` is caller-supplied
2033    /// persisted state rather than a live authenticated channel.
2034    #[allow(clippy::type_complexity)]
2035    pub fn resume<M: Faults>(
2036        info: Info<V, S::PublicKey>,
2037        me: S,
2038        logs: &BTreeMap<S::PublicKey, DealerLog<V, S::PublicKey>>,
2039        msgs: impl IntoIterator<Item = (S::PublicKey, DealerPubMsg<V>, DealerPrivMsg)>,
2040    ) -> Result<(Self, BTreeMap<S::PublicKey, PlayerAck<S::PublicKey>>), Error> {
2041        // Record all acks we've emitted (by dealer).
2042        let mut this = Self::new(info, me)?;
2043        let mut acks = BTreeMap::new();
2044        for (dealer, pub_msg, priv_msg) in msgs {
2045            match this.dealer_message::<M>(dealer.clone(), pub_msg, priv_msg) {
2046                Ok(Some(ack)) => {
2047                    acks.insert(dealer, ack);
2048                }
2049                Ok(None) => {}
2050                Err(_) => {
2051                    return Err(Error::InvalidPersistedDealing {
2052                        dealer: format!("{dealer:?}"),
2053                    });
2054                }
2055            }
2056        }
2057
2058        // Have we emitted any valid acks, publicly recorded, for which we do
2059        // not have a private message from the dealer?
2060        if logs.iter().any(|(dealer, log)| {
2061            let Some(ack) = log.get_ack(&this.me_pub) else {
2062                return false;
2063            };
2064            // Only trust this ack if the signature is valid for this round.
2065            transcript_for_ack(&this.transcript, dealer, &log.pub_msg)
2066                .verify(&this.me_pub, &ack.sig)
2067                && !this.view.contains_key(dealer)
2068        }) {
2069            // If so, we have a problem, because we're missing a dealing that we're
2070            // supposed to have, and that we publicly committed to having.
2071            return Err(Error::MissingPlayerDealing);
2072        }
2073
2074        Ok((this, acks))
2075    }
2076
2077    /// Process a message from a dealer.
2078    ///
2079    /// It's important that nobody can impersonate the dealer, and that the
2080    /// private message was not exposed to anyone else. A convenient way to
2081    /// provide this is by using an authenticated channel, e.g. via
2082    /// [crate::handshake], or [commonware-p2p](https://docs.rs/commonware-p2p/latest/commonware_p2p/).
2083    ///
2084    /// Returns [`DealerMessageError`] if the message is invalid, `Ok(None)` if a
2085    /// message from this dealer was already processed, and `Ok(Some(_))` with
2086    /// the acknowledgement otherwise. The error carries no attribution. A
2087    /// transport-aware caller can apply its own attribution policy to `dealer`.
2088    pub fn dealer_message<M: Faults>(
2089        &mut self,
2090        dealer: S::PublicKey,
2091        pub_msg: DealerPubMsg<V>,
2092        priv_msg: DealerPrivMsg,
2093    ) -> Result<Option<PlayerAck<S::PublicKey>>, DealerMessageError> {
2094        if self.view.contains_key(&dealer) {
2095            return Ok(None);
2096        }
2097        if self.info.dealer_index(&dealer).is_none() {
2098            return Err(DealerMessageError::UnexpectedDealer);
2099        }
2100        self.info
2101            .check_dealer_pub_msg::<M>(&dealer, &pub_msg)
2102            .map_err(DealerMessageError::from)?;
2103        if !self
2104            .info
2105            .check_dealer_priv_msg(self.index, &pub_msg, &priv_msg)
2106        {
2107            return Err(DealerMessageError::InvalidDealerShare);
2108        }
2109        let sig = transcript_for_ack(&self.transcript, &dealer, &pub_msg).sign(&self.me);
2110        self.view.insert(dealer, (pub_msg, priv_msg));
2111        Ok(Some(PlayerAck { sig }))
2112    }
2113
2114    /// Finalize the player, producing an output, and a share.
2115    ///
2116    /// This should agree with [`observe`], in terms of `Ok` vs `Err` (with one exception)
2117    /// and the public output, so long as the logs agree. It's crucial that the players
2118    /// come to agreement, in some way, on exactly which logs they need to use
2119    /// for finalize.
2120    ///
2121    /// The exception is that if this function returns [`FinalizeError::Error`]
2122    /// containing [`Error::MissingPlayerDealing`] or
2123    /// [`Error::InvalidPersistedDealing`], then [`observe`] will return `Ok`,
2124    /// because these errors indicate that this player's state has been corrupted,
2125    /// but the DKG has otherwise succeeded. However, this player's share is not
2126    /// recoverable without external intervention.
2127    ///
2128    /// Otherwise, this function returns [`FinalizeError::Failure`] if the agreed
2129    /// dealer logs cannot produce a DKG output, or
2130    /// [`FinalizeError::Error`] containing [`Error::MismatchedLogs`] if `logs` are
2131    /// bound to a different DKG round.
2132    #[allow(clippy::type_complexity)]
2133    pub fn finalize<M: Faults, B: BatchVerifier<PublicKey = S::PublicKey>>(
2134        self,
2135        rng: &mut impl CryptoRng,
2136        logs: Logs<V, S::PublicKey, M>,
2137        strategy: &impl Strategy,
2138    ) -> Result<(Output<V, S::PublicKey>, Share), FinalizeError<S::PublicKey>> {
2139        if logs.info != self.info {
2140            return Err(Error::MismatchedLogs.into());
2141        }
2142        let (_, selected) = logs.select::<B>(rng, strategy)?;
2143
2144        // We are extracting the private scalars from `Secret` protection
2145        // because interpolation/summation needs owned scalars for polynomial
2146        // arithmetic. The extracted scalars are scoped to this function and
2147        // will be zeroized on drop (i.e. the secrets are only exposed for the
2148        // duration of this function).
2149        let dealings = selected
2150            .iter_pairs()
2151            .map(|(dealer, log)| {
2152                // A selected ack carries no share and requires the exact persisted
2153                // dealing. A validated reveal can replace missing or stale local state.
2154                let persisted = self.view.get(dealer);
2155                let share = match persisted {
2156                    Some((pub_msg, priv_msg)) if pub_msg == &log.pub_msg => {
2157                        priv_msg.share.clone().expose_unwrap()
2158                    }
2159                    _ => match log.get_reveal(&self.me_pub) {
2160                        Some(priv_msg) => priv_msg.share.clone().expose_unwrap(),
2161                        None if persisted.is_some() => {
2162                            return Err(Error::InvalidPersistedDealing {
2163                                dealer: format!("{dealer:?}"),
2164                            });
2165                        }
2166                        None => return Err(Error::MissingPlayerDealing),
2167                    },
2168                };
2169                Ok((dealer.clone(), share))
2170            })
2171            .collect::<Result<Vec<_>, Error>>()?
2172            .into_iter()
2173            .try_collect::<Map<_, _>>()
2174            .expect("Logs::select produces at most one entry per dealer");
2175        let Observe { output, weights } =
2176            Observe::<V, S::PublicKey>::reckon::<M>(self.info, selected, strategy);
2177        let private = weights.map_or_else(
2178            || {
2179                let mut out = <Scalar as Additive>::zero();
2180                for s in dealings.values() {
2181                    out += s;
2182                }
2183                out
2184            },
2185            |weights| {
2186                weights
2187                    .interpolate(&dealings, strategy)
2188                    .expect("Logs::select ensures that we can recover")
2189            },
2190        );
2191        let share = Share::new(self.index, Private::new(private));
2192        Ok((output, share))
2193    }
2194}
2195
2196/// The result of dealing shares to players.
2197pub type DealResult<V, P> = Result<(Output<V, P>, Map<P, Share>), Error>;
2198
2199/// Simply distribute shares at random, instead of performing a distributed protocol.
2200pub fn deal<V: Variant, P: Clone + Ord, M: Faults>(
2201    mut rng: impl CryptoRng,
2202    mode: Mode,
2203    players: Set<P>,
2204) -> DealResult<V, P> {
2205    if players.is_empty() {
2206        return Err(Error::NumPlayers(0));
2207    }
2208    let n = NZU32!(players.len() as u32);
2209    let t = players.quorum::<M>();
2210    let private = Poly::new(&mut rng, t - 1);
2211    let shares: Map<_, _> = players
2212        .iter()
2213        .enumerate()
2214        .map(|(i, p)| {
2215            let participant = Participant::from_usize(i);
2216            let eval = private.eval_msm(
2217                &mode
2218                    .scalar(n, participant)
2219                    .expect("player index should be valid"),
2220                &Sequential,
2221            );
2222            let share = Share::new(participant, Private::new(eval));
2223            (p.clone(), share)
2224        })
2225        .try_collect()
2226        .expect("players are unique");
2227    let output = Output {
2228        summary: Summary::random(&mut rng),
2229        public: Sharing::new(mode, n, Poly::commit(private)),
2230        dealers: players.clone(),
2231        players,
2232        revealed: Set::default(),
2233    };
2234    Ok((output, shares))
2235}
2236
2237/// Like [`deal`], but without linking the result to specific public keys.
2238///
2239/// This can be more convenient for testing, where you don't want to go through
2240/// the trouble of generating signing keys. The downside is that the result isn't
2241/// compatible with subsequent DKGs, which need an [`Output`].
2242pub fn deal_anonymous<V: Variant, M: Faults>(
2243    rng: impl CryptoRng,
2244    mode: Mode,
2245    n: NonZeroU32,
2246) -> (Sharing<V>, Vec<Share>) {
2247    let players = (0..n.get()).try_collect().unwrap();
2248    let (output, shares) = deal::<V, _, M>(rng, mode, players).unwrap();
2249    (output.public().clone(), shares.values().to_vec())
2250}
2251
2252#[cfg(any(feature = "arbitrary", test))]
2253mod test_plan {
2254    use super::*;
2255    use crate::{
2256        PublicKey,
2257        bls12381::primitives::{
2258            ops::{self, threshold},
2259            variant::Variant,
2260        },
2261        ed25519,
2262    };
2263    use anyhow::anyhow;
2264    use bytes::BytesMut;
2265    use commonware_utils::{Faults, N3f1, TestRng, TryCollect};
2266    use core::num::NonZeroI32;
2267    use std::collections::BTreeSet;
2268
2269    /// Apply a mask to some bytes, returning whether or not a modification happened
2270    fn apply_mask(bytes: &mut BytesMut, mask: &[u8]) -> bool {
2271        let mut modified = false;
2272        for (l, &r) in bytes.iter_mut().zip(mask.iter()) {
2273            modified |= r != 0;
2274            *l ^= r;
2275        }
2276        modified
2277    }
2278
2279    #[derive(Clone, Default, Debug)]
2280    pub struct Masks {
2281        pub info_summary: Vec<u8>,
2282        pub dealer: Vec<u8>,
2283        pub pub_msg: Vec<u8>,
2284        pub log: Vec<u8>,
2285    }
2286
2287    impl Masks {
2288        fn modifies_player_ack(&self) -> bool {
2289            self.info_summary.iter().any(|&b| b != 0)
2290                || self.dealer.iter().any(|&b| b != 0)
2291                || self.pub_msg.iter().any(|&b| b != 0)
2292        }
2293
2294        fn transcript_for_round<V: Variant, P: PublicKey>(
2295            &self,
2296            info: &Info<V, P>,
2297        ) -> anyhow::Result<(bool, Transcript)> {
2298            let mut summary_bs = info.summary.encode_mut();
2299            let modified = apply_mask(&mut summary_bs, &self.info_summary);
2300            let summary = Summary::read(&mut summary_bs)?;
2301            Ok((modified, Transcript::resume(summary, TRANSCRIPT_VERSION)))
2302        }
2303
2304        fn transcript_for_player_ack<V: Variant, P: PublicKey>(
2305            &self,
2306            info: &Info<V, P>,
2307            dealer: &P,
2308            pub_msg: &DealerPubMsg<V>,
2309        ) -> anyhow::Result<(bool, Transcript)> {
2310            let (mut modified, transcript) = self.transcript_for_round(info)?;
2311            let mut transcript = transcript.fork(SIG_ACK);
2312
2313            let mut dealer_bs = dealer.encode_mut();
2314            modified |= apply_mask(&mut dealer_bs, &self.dealer);
2315            transcript.commit(&mut dealer_bs);
2316
2317            let mut pub_msg_bs = pub_msg.encode_mut();
2318            modified |= apply_mask(&mut pub_msg_bs, &self.pub_msg);
2319            transcript.commit(&mut pub_msg_bs);
2320
2321            Ok((modified, transcript))
2322        }
2323
2324        fn transcript_for_signed_dealer_log<V: Variant, P: PublicKey>(
2325            &self,
2326            info: &Info<V, P>,
2327            log: &DealerLog<V, P>,
2328        ) -> anyhow::Result<(bool, Transcript)> {
2329            let (mut modified, transcript) = self.transcript_for_round(info)?;
2330            let mut transcript = transcript.fork(SIG_LOG);
2331
2332            let mut log_bs = log.encode_mut();
2333            modified |= apply_mask(&mut log_bs, &self.log);
2334            transcript.commit(&mut log_bs);
2335
2336            Ok((modified, transcript))
2337        }
2338    }
2339
2340    /// A round in the DKG test plan.
2341    #[derive(Debug, Default)]
2342    pub struct Round {
2343        dealers: Vec<u32>,
2344        players: Vec<u32>,
2345        crash_resume_players: BTreeSet<(u32, u32)>,
2346        resume_missing_dealer_msg_fails: BTreeSet<(u32, u32)>,
2347        finalize_missing_dealer_msg_fails: BTreeSet<u32>,
2348        no_acks: BTreeSet<(u32, u32)>,
2349        bad_shares: BTreeSet<(u32, u32)>,
2350        bad_player_sigs: BTreeMap<(u32, u32), Masks>,
2351        bad_reveals: BTreeSet<(u32, u32)>,
2352        bad_dealer_sigs: BTreeMap<u32, Masks>,
2353        replace_shares: BTreeSet<u32>,
2354        shift_degrees: BTreeMap<u32, NonZeroI32>,
2355    }
2356
2357    impl Round {
2358        pub fn new(dealers: Vec<u32>, players: Vec<u32>) -> Self {
2359            Self {
2360                dealers,
2361                players,
2362                ..Default::default()
2363            }
2364        }
2365
2366        pub fn no_ack(mut self, dealer: u32, player: u32) -> Self {
2367            self.no_acks.insert((dealer, player));
2368            self
2369        }
2370
2371        pub fn crash_resume_player(mut self, after_dealer: u32, player: u32) -> Self {
2372            self.crash_resume_players.insert((after_dealer, player));
2373            self
2374        }
2375
2376        pub fn resume_missing_dealer_msg_fails(
2377            mut self,
2378            after_dealer: u32,
2379            missing_dealer: u32,
2380        ) -> Self {
2381            self.resume_missing_dealer_msg_fails
2382                .insert((after_dealer, missing_dealer));
2383            self
2384        }
2385
2386        pub fn finalize_missing_dealer_msg_fails(mut self, player: u32) -> Self {
2387            self.finalize_missing_dealer_msg_fails.insert(player);
2388            self
2389        }
2390
2391        pub fn bad_share(mut self, dealer: u32, player: u32) -> Self {
2392            self.bad_shares.insert((dealer, player));
2393            self
2394        }
2395
2396        pub fn bad_player_sig(mut self, dealer: u32, player: u32, masks: Masks) -> Self {
2397            self.bad_player_sigs.insert((dealer, player), masks);
2398            self
2399        }
2400
2401        pub fn bad_reveal(mut self, dealer: u32, player: u32) -> Self {
2402            self.bad_reveals.insert((dealer, player));
2403            self
2404        }
2405
2406        pub fn bad_dealer_sig(mut self, dealer: u32, masks: Masks) -> Self {
2407            self.bad_dealer_sigs.insert(dealer, masks);
2408            self
2409        }
2410
2411        pub fn replace_share(mut self, dealer: u32) -> Self {
2412            self.replace_shares.insert(dealer);
2413            self
2414        }
2415
2416        pub fn shift_degree(mut self, dealer: u32, shift: NonZeroI32) -> Self {
2417            self.shift_degrees.insert(dealer, shift);
2418            self
2419        }
2420
2421        /// Validate that this round is well-formed given the number of participants
2422        /// and the previous successful round's players.
2423        pub fn validate(
2424            &self,
2425            num_participants: u32,
2426            previous_players: Option<&[u32]>,
2427        ) -> anyhow::Result<()> {
2428            if self.dealers.is_empty() {
2429                return Err(anyhow!("dealers is empty"));
2430            }
2431            if self.players.is_empty() {
2432                return Err(anyhow!("players is empty"));
2433            }
2434            // Check dealer/player ranges
2435            for &d in &self.dealers {
2436                if d >= num_participants {
2437                    return Err(anyhow!("dealer {d} out of range [1, {num_participants}]"));
2438                }
2439            }
2440            for &p in &self.players {
2441                if p >= num_participants {
2442                    return Err(anyhow!("player {p} out of range [1, {num_participants}]"));
2443                }
2444            }
2445            // Crash/resume checkpoints must reference in-round dealers/players.
2446            for &(after_dealer, player) in &self.crash_resume_players {
2447                if !self.dealers.contains(&after_dealer) {
2448                    return Err(anyhow!("crash_resume dealer {after_dealer} not in round"));
2449                }
2450                if !self.players.contains(&player) {
2451                    return Err(anyhow!("crash_resume player {player} not in round"));
2452                }
2453            }
2454            let dealer_positions: BTreeMap<u32, usize> = self
2455                .dealers
2456                .iter()
2457                .enumerate()
2458                .map(|(idx, &dealer)| (dealer, idx))
2459                .collect();
2460            let previous_successful_round = previous_players.is_some();
2461            for &(after_dealer, missing_dealer) in &self.resume_missing_dealer_msg_fails {
2462                if !self.dealers.contains(&after_dealer) {
2463                    return Err(anyhow!("resume_missing dealer {after_dealer} not in round"));
2464                }
2465                if !self.dealers.contains(&missing_dealer) {
2466                    return Err(anyhow!(
2467                        "resume_missing missing_dealer {missing_dealer} not in round"
2468                    ));
2469                }
2470                let after_pos = dealer_positions[&after_dealer];
2471                let missing_pos = dealer_positions[&missing_dealer];
2472                if missing_pos > after_pos {
2473                    return Err(anyhow!(
2474                        "resume_missing missing_dealer {missing_dealer} appears after {after_dealer}"
2475                    ));
2476                }
2477                if self.bad(previous_successful_round, missing_dealer) {
2478                    return Err(anyhow!(
2479                        "resume_missing_dealer_msg_fails requires dealer {missing_dealer} to be good"
2480                    ));
2481                }
2482                let any_valid_ack = self.players.iter().any(|&player| {
2483                    let ack_corrupted = self.no_acks.contains(&(missing_dealer, player))
2484                        || self.bad_shares.contains(&(missing_dealer, player))
2485                        || self
2486                            .bad_player_sigs
2487                            .get(&(missing_dealer, player))
2488                            .is_some_and(Masks::modifies_player_ack);
2489                    !ack_corrupted
2490                });
2491                if !any_valid_ack {
2492                    return Err(anyhow!(
2493                        "resume_missing_dealer_msg_fails requires dealer {missing_dealer} to ack at least one player"
2494                    ));
2495                }
2496            }
2497            for &player in &self.finalize_missing_dealer_msg_fails {
2498                if !self.players.contains(&player) {
2499                    return Err(anyhow!("finalize_missing player {player} not in round"));
2500                }
2501            }
2502
2503            // If there's a previous round, check dealer constraints
2504            if let Some(prev_players) = previous_players {
2505                // Every dealer must have been a player in the previous round
2506                for &d in &self.dealers {
2507                    if !prev_players.contains(&d) {
2508                        return Err(anyhow!("dealer {d} was not a player in previous round"));
2509                    }
2510                }
2511                // Must have >= quorum(prev_players) dealers
2512                let required = N3f1::quorum(prev_players.len());
2513                if (self.dealers.len() as u32) < required {
2514                    return Err(anyhow!(
2515                        "not enough dealers: have {}, need {} (quorum of {} previous players)",
2516                        self.dealers.len(),
2517                        required,
2518                        prev_players.len()
2519                    ));
2520                }
2521            }
2522
2523            Ok(())
2524        }
2525
2526        fn bad(&self, previous_successful_round: bool, dealer: u32) -> bool {
2527            if self.replace_shares.contains(&dealer) && previous_successful_round {
2528                return true;
2529            }
2530            if let Some(shift) = self.shift_degrees.get(&dealer) {
2531                let degree = N3f1::quorum(self.players.len()) as i32 - 1;
2532                // We shift the degree, but saturate at 0, so it's possible
2533                // that the shift isn't actually doing anything.
2534                //
2535                // This is effectively the same as checking degree == 0 && shift < 0,
2536                // but matches what ends up happening a bit better.
2537                if (degree + shift.get()).max(0) != degree {
2538                    return true;
2539                }
2540            }
2541            if self.bad_reveals.iter().any(|&(d, _)| d == dealer) {
2542                return true;
2543            }
2544            let revealed_players = self
2545                .bad_shares
2546                .iter()
2547                .copied()
2548                .chain(self.no_acks.iter().copied())
2549                .filter_map(|(d, p)| if d == dealer { Some(p) } else { None })
2550                .collect::<BTreeSet<_>>();
2551            revealed_players.len() as u32 > N3f1::max_faults(self.players.len())
2552        }
2553
2554        /// Determine if this round is expected to fail.
2555        fn expect_failure(&self, previous_successful_round: Option<u32>) -> bool {
2556            let good_dealer_count = self
2557                .dealers
2558                .iter()
2559                .filter(|&&d| !self.bad(previous_successful_round.is_some(), d))
2560                .count();
2561            let required = previous_successful_round
2562                .map(N3f1::quorum)
2563                .unwrap_or_default()
2564                .max(N3f1::quorum(self.dealers.len())) as usize;
2565            good_dealer_count < required
2566        }
2567    }
2568
2569    /// A DKG test plan consisting of multiple rounds.
2570    #[derive(Debug)]
2571    pub struct Plan {
2572        num_participants: NonZeroU32,
2573        rounds: Vec<Round>,
2574    }
2575
2576    impl Plan {
2577        pub const fn new(num_participants: NonZeroU32) -> Self {
2578            Self {
2579                num_participants,
2580                rounds: Vec::new(),
2581            }
2582        }
2583
2584        pub fn with(mut self, round: Round) -> Self {
2585            self.rounds.push(round);
2586            self
2587        }
2588
2589        /// Validate the entire plan.
2590        pub(crate) fn validate(&self) -> anyhow::Result<()> {
2591            let mut last_successful_players: Option<Vec<u32>> = None;
2592
2593            for round in &self.rounds {
2594                round.validate(
2595                    self.num_participants.get(),
2596                    last_successful_players.as_deref(),
2597                )?;
2598
2599                // If this round is expected to succeed, update last_successful_players
2600                if !round.expect_failure(last_successful_players.as_ref().map(|x| x.len() as u32)) {
2601                    last_successful_players = Some(round.players.clone());
2602                }
2603            }
2604            Ok(())
2605        }
2606
2607        /// Run the test plan with a given seed.
2608        pub fn run<V: Variant>(self, seed: u64) -> anyhow::Result<()> {
2609            self.validate()?;
2610
2611            let mut rng = TestRng::new(seed);
2612
2613            // Generate keys for all participants (1-indexed to num_participants)
2614            let keys = (0..self.num_participants.get())
2615                .map(|_| ed25519::PrivateKey::random(&mut rng))
2616                .collect::<Vec<_>>();
2617
2618            // Precompute mapping from public key to key index to avoid confusion
2619            // between key indices and positions in sorted Sets.
2620            let pk_to_key_idx: BTreeMap<ed25519::PublicKey, u32> = keys
2621                .iter()
2622                .enumerate()
2623                .map(|(i, k)| (k.public_key(), i as u32))
2624                .collect();
2625
2626            // The max_read_size needs to account for shifted polynomial degrees.
2627            // Find the maximum positive shift across all rounds.
2628            let max_shift = self
2629                .rounds
2630                .iter()
2631                .flat_map(|r| r.shift_degrees.values())
2632                .map(|s| s.get())
2633                .max()
2634                .unwrap_or(0)
2635                .max(0) as u32;
2636            let max_read_size =
2637                NonZeroU32::new(self.num_participants.get() + max_shift).expect("non-zero");
2638
2639            let mut previous_output: Option<Output<V, ed25519::PublicKey>> = None;
2640            let mut shares: BTreeMap<ed25519::PublicKey, Share> = BTreeMap::new();
2641            let mut threshold_public_key: Option<V::Public> = None;
2642
2643            for (i_round, round) in self.rounds.into_iter().enumerate() {
2644                let previous_successful_round =
2645                    previous_output.as_ref().map(|o| o.players.len() as u32);
2646
2647                let dealer_set = round
2648                    .dealers
2649                    .iter()
2650                    .map(|&i| keys[i as usize].public_key())
2651                    .try_collect::<Set<_>>()
2652                    .unwrap();
2653                let player_set: Set<ed25519::PublicKey> = round
2654                    .players
2655                    .iter()
2656                    .map(|&i| keys[i as usize].public_key())
2657                    .try_collect()
2658                    .unwrap();
2659
2660                // Create round info
2661                let info = Info::new::<N3f1>(
2662                    b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
2663                    i_round as u64,
2664                    previous_output.clone(),
2665                    Mode::NonZeroCounter,
2666                    Reveal::V1,
2667                    dealer_set.clone(),
2668                    player_set.clone(),
2669                )?;
2670
2671                let mut players: Map<_, _> = round
2672                    .players
2673                    .iter()
2674                    .map(|&i| {
2675                        let sk = keys[i as usize].clone();
2676                        let pk = sk.public_key();
2677                        let player = Player::new(info.clone(), sk)?;
2678                        Ok((pk, player))
2679                    })
2680                    .collect::<anyhow::Result<Vec<_>>>()?
2681                    .try_into()
2682                    .unwrap();
2683                let mut acked_dealings: BTreeMap<
2684                    ed25519::PublicKey,
2685                    Vec<(ed25519::PublicKey, DealerPubMsg<V>, DealerPrivMsg)>,
2686                > = player_set
2687                    .iter()
2688                    .cloned()
2689                    .map(|pk| (pk, Vec::new()))
2690                    .collect();
2691                let mut crash_resume_by_dealer: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
2692                for &(after_dealer, player) in &round.crash_resume_players {
2693                    crash_resume_by_dealer
2694                        .entry(after_dealer)
2695                        .or_default()
2696                        .push(player);
2697                }
2698                let mut resume_missing_msg_by_dealer: BTreeMap<u32, Vec<u32>> = BTreeMap::new();
2699                for &(after_dealer, missing_dealer) in &round.resume_missing_dealer_msg_fails {
2700                    resume_missing_msg_by_dealer
2701                        .entry(after_dealer)
2702                        .or_default()
2703                        .push(missing_dealer);
2704                }
2705
2706                // Run dealer protocol
2707                let mut dealer_logs = BTreeMap::new();
2708                for &i_dealer in &round.dealers {
2709                    let sk = keys[i_dealer as usize].clone();
2710                    let pk = sk.public_key();
2711                    let share = match (shares.get(&pk), round.replace_shares.contains(&i_dealer)) {
2712                        (None, _) => None,
2713                        (Some(s), false) => Some(s.clone()),
2714                        (Some(_), true) => Some(Share::new(
2715                            Participant::new(i_dealer),
2716                            Private::random(&mut rng),
2717                        )),
2718                    };
2719
2720                    // Start dealer (with potential modifications)
2721                    let (mut dealer, pub_msg, mut priv_msgs) =
2722                        if let Some(shift) = round.shift_degrees.get(&i_dealer) {
2723                            // Create dealer with shifted degree
2724                            let degree = u32::try_from(info.degree::<N3f1>() as i32 + shift.get())
2725                                .unwrap_or_default();
2726
2727                            // Manually create the dealer with adjusted polynomial
2728                            let share = info
2729                                .unwrap_or_random_share(
2730                                    &mut rng,
2731                                    share.map(|s| s.private.expose_unwrap()),
2732                                )
2733                                .expect("Failed to generate dealer share");
2734
2735                            let my_poly = Poly::new_with_constant(&mut rng, degree, share);
2736                            let priv_msgs = info
2737                                .players
2738                                .iter()
2739                                .map(|pk| {
2740                                    (
2741                                        pk.clone(),
2742                                        DealerPrivMsg::new(my_poly.eval_msm(
2743                                            &info.player_scalar(pk).expect("player should exist"),
2744                                            &Sequential,
2745                                        )),
2746                                    )
2747                                })
2748                                .collect::<Vec<_>>();
2749                            let results: Map<_, _> = priv_msgs
2750                                .iter()
2751                                .map(|(pk, pm)| (pk.clone(), AckOrReveal::Reveal(pm.clone())))
2752                                .try_collect()
2753                                .unwrap();
2754                            let commitment = Poly::commit(my_poly);
2755                            let pub_msg = DealerPubMsg { commitment };
2756                            let transcript = {
2757                                let t = transcript_for_round(&info);
2758                                transcript_for_ack(&t, &pk, &pub_msg)
2759                            };
2760                            let dealer = Dealer {
2761                                me: sk.clone(),
2762                                info: info.clone(),
2763                                pub_msg: pub_msg.clone(),
2764                                results,
2765                                transcript,
2766                            };
2767                            (dealer, pub_msg, priv_msgs)
2768                        } else {
2769                            Dealer::start::<N3f1>(&mut rng, info.clone(), sk.clone(), share)?
2770                        };
2771
2772                    // Apply BadShare perturbations
2773                    for (player, priv_msg) in &mut priv_msgs {
2774                        let player_key_idx = pk_to_key_idx[player];
2775                        if round.bad_shares.contains(&(i_dealer, player_key_idx)) {
2776                            *priv_msg = DealerPrivMsg::new(Scalar::random(&mut rng));
2777                        }
2778                    }
2779                    assert_eq!(priv_msgs.len(), players.len());
2780
2781                    // Process player acks
2782                    let mut num_reveals = players.len() as u32;
2783                    for (player_pk, priv_msg) in priv_msgs {
2784                        // Check priv msg encoding.
2785                        assert_eq!(priv_msg, ReadExt::read(&mut priv_msg.encode())?);
2786
2787                        let i_player = players
2788                            .index(&player_pk)
2789                            .ok_or_else(|| anyhow!("unknown player: {:?}", player_pk))?;
2790                        let player_key_idx = pk_to_key_idx[&player_pk];
2791                        let player = &mut players.values_mut()[usize::from(i_player)];
2792                        let persisted = priv_msg.clone();
2793
2794                        let ack = player
2795                            .dealer_message::<N3f1>(pk.clone(), pub_msg.clone(), priv_msg)
2796                            .ok()
2797                            .flatten();
2798                        assert_eq!(ack, ReadExt::read(&mut ack.encode())?);
2799                        if let Some(ack) = ack {
2800                            acked_dealings
2801                                .get_mut(&player_pk)
2802                                .expect("player should be present")
2803                                .push((pk.clone(), pub_msg.clone(), persisted));
2804                            let masks = round
2805                                .bad_player_sigs
2806                                .get(&(i_dealer, player_key_idx))
2807                                .cloned()
2808                                .unwrap_or_default();
2809                            let (modified, transcript) =
2810                                masks.transcript_for_player_ack(&info, &pk, &pub_msg)?;
2811                            assert_eq!(transcript.verify(&player_pk, &ack.sig), !modified);
2812
2813                            // Skip receiving ack if NoAck perturbation
2814                            if !round.no_acks.contains(&(i_dealer, player_key_idx)) {
2815                                dealer.receive_player_ack(player_pk, ack)?;
2816                                num_reveals -= 1;
2817                            }
2818                        } else {
2819                            assert!(
2820                                round.bad_shares.contains(&(i_dealer, player_key_idx))
2821                                    || round.bad(previous_successful_round.is_some(), i_dealer)
2822                            );
2823                        }
2824                    }
2825
2826                    // Finalize dealer
2827                    let signed_log = dealer.finalize::<N3f1>();
2828                    assert_eq!(
2829                        signed_log,
2830                        Read::read_cfg(&mut signed_log.encode(), &max_read_size)?
2831                    );
2832
2833                    // Check for BadDealerSig
2834                    let masks = round
2835                        .bad_dealer_sigs
2836                        .get(&i_dealer)
2837                        .cloned()
2838                        .unwrap_or_default();
2839                    let (modified, transcript) =
2840                        masks.transcript_for_signed_dealer_log(&info, &signed_log.log)?;
2841                    assert_eq!(transcript.verify(&pk, &signed_log.sig), !modified);
2842                    let (found_pk, mut log) = signed_log
2843                        .check(&info)
2844                        .ok_or_else(|| anyhow!("signed log should verify"))?;
2845                    assert_eq!(pk, found_pk);
2846                    // Apply BadReveal perturbations
2847                    match &mut log.results {
2848                        DealerResult::TooManyReveals => {
2849                            assert!(num_reveals > info.max_reveals::<N3f1>());
2850                        }
2851                        DealerResult::Ok(results) => {
2852                            assert_eq!(results.len(), players.len());
2853                            for &i_player in &round.players {
2854                                if !round.bad_reveals.contains(&(i_dealer, i_player)) {
2855                                    continue;
2856                                }
2857                                let player_pk = keys[i_player as usize].public_key();
2858                                *results
2859                                    .get_value_mut(&player_pk)
2860                                    .ok_or_else(|| anyhow!("unknown player: {:?}", player_pk))? =
2861                                    AckOrReveal::Reveal(DealerPrivMsg::new(Scalar::random(
2862                                        &mut rng,
2863                                    )));
2864                            }
2865                        }
2866                    }
2867                    dealer_logs.insert(pk, log);
2868
2869                    // For selected checkpoints, omit a good dealer's private message and
2870                    // ensure resume reports corruption. Do not mutate player state.
2871                    for &missing_dealer in resume_missing_msg_by_dealer
2872                        .get(&i_dealer)
2873                        .into_iter()
2874                        .flatten()
2875                    {
2876                        assert!(
2877                            !round.bad(previous_successful_round.is_some(), missing_dealer),
2878                            "resume_missing_dealer_msg_fails requires dealer {missing_dealer} to be good"
2879                        );
2880                        let missing_pk = keys[missing_dealer as usize].public_key();
2881                        let missing_log = dealer_logs
2882                            .get(&missing_pk)
2883                            .unwrap_or_else(|| panic!("missing dealer log for {:?}", missing_pk));
2884                        for &i_player in &round.players {
2885                            let player_pk = keys[i_player as usize].public_key();
2886                            let was_acked = missing_log.get_ack(&player_pk).is_some();
2887
2888                            let replay = acked_dealings
2889                                .get(&player_pk)
2890                                .cloned()
2891                                .expect("player should be present");
2892                            let replay_without = replay
2893                                .into_iter()
2894                                .filter(|(dealer, _, _)| dealer != &missing_pk);
2895                            let player_sk = keys[i_player as usize].clone();
2896                            let resumed = Player::resume::<N3f1>(
2897                                info.clone(),
2898                                player_sk,
2899                                &dealer_logs,
2900                                replay_without,
2901                            );
2902                            if was_acked {
2903                                assert!(
2904                                    matches!(resumed, Err(Error::MissingPlayerDealing)),
2905                                    "resume without dealer {missing_dealer} message should report MissingPlayerDealing for player {i_player}"
2906                                );
2907                            } else {
2908                                assert!(
2909                                    resumed.is_ok(),
2910                                    "resume without dealer {missing_dealer} message should succeed for unacked player {i_player}"
2911                                );
2912                            }
2913                        }
2914                    }
2915
2916                    // Crash/resume selected players after this dealer has finalized.
2917                    for &i_player in crash_resume_by_dealer.get(&i_dealer).into_iter().flatten() {
2918                        let player_pk = keys[i_player as usize].public_key();
2919                        let player_sk = keys[i_player as usize].clone();
2920                        let replay = acked_dealings
2921                            .get(&player_pk)
2922                            .cloned()
2923                            .expect("player should be present");
2924                        let (resumed, _) =
2925                            Player::resume::<N3f1>(info.clone(), player_sk, &dealer_logs, replay)
2926                                .expect("player resume perturbation should succeed");
2927                        *players
2928                            .get_value_mut(&player_pk)
2929                            .expect("player should be present") = resumed;
2930                    }
2931                }
2932
2933                // Make sure that bad dealers are not selected.
2934                let mut logs = Logs::<_, _, N3f1>::new(info.clone());
2935                for (dealer, log) in &dealer_logs {
2936                    logs.record(dealer.clone(), log.clone());
2937                }
2938                let selection = logs.clone().select::<ed25519::Batch>(&mut rng, &Sequential);
2939                if let Ok(ref selection) = selection {
2940                    let good_pks = selection
2941                        .1
2942                        .iter_pairs()
2943                        .map(|(pk, _)| pk.clone())
2944                        .collect::<BTreeSet<_>>();
2945                    for &i_dealer in &round.dealers {
2946                        if round.bad(previous_successful_round.is_some(), i_dealer) {
2947                            assert!(!good_pks.contains(&keys[i_dealer as usize].public_key()));
2948                        }
2949                    }
2950                }
2951                // Run observer
2952                let observe_result =
2953                    observe::<_, _, N3f1, ed25519::Batch>(&mut rng, logs.clone(), &Sequential);
2954                if round.expect_failure(previous_successful_round) {
2955                    assert!(
2956                        observe_result.is_err(),
2957                        "Round {i_round} should have failed but succeeded",
2958                    );
2959                    continue;
2960                }
2961                let observer_output = observe_result?;
2962                let selection = selection.expect("select should succeed if observe succeeded");
2963
2964                // Compute expected dealers: good dealers up to required_commitments
2965                // The select function iterates dealer_logs (BTreeMap) in public key order
2966                let required_commitments = info.required_commitments::<N3f1>() as usize;
2967                let expected_dealers: Set<ed25519::PublicKey> = dealer_set
2968                    .iter()
2969                    .filter(|pk| {
2970                        let i = keys.iter().position(|k| &k.public_key() == *pk).unwrap() as u32;
2971                        !round.bad(previous_successful_round.is_some(), i)
2972                    })
2973                    .take(required_commitments)
2974                    .cloned()
2975                    .try_collect()
2976                    .expect("dealers are unique");
2977                let expected_dealer_indices: BTreeSet<u32> = expected_dealers
2978                    .iter()
2979                    .filter_map(|pk| {
2980                        keys.iter()
2981                            .position(|k| &k.public_key() == pk)
2982                            .map(|i| i as u32)
2983                    })
2984                    .collect();
2985                assert_eq!(
2986                    observer_output.dealers(),
2987                    &expected_dealers,
2988                    "Output dealers should match expected good dealers"
2989                );
2990
2991                // Map selected dealers to their key indices (for later use)
2992                let selected_dealers: BTreeSet<u32> = selection
2993                    .1
2994                    .keys()
2995                    .iter()
2996                    .filter_map(|pk| {
2997                        keys.iter()
2998                            .position(|k| &k.public_key() == pk)
2999                            .map(|i| i as u32)
3000                    })
3001                    .collect();
3002                assert_eq!(
3003                    selected_dealers, expected_dealer_indices,
3004                    "Selection should match expected dealers"
3005                );
3006                let selected_players: Set<ed25519::PublicKey> = round
3007                    .players
3008                    .iter()
3009                    .map(|&i| keys[i as usize].public_key())
3010                    .try_collect()
3011                    .expect("players are unique");
3012                for &i_player in &round.finalize_missing_dealer_msg_fails {
3013                    let player_pk = keys[i_player as usize].public_key();
3014                    let player_sk = keys[i_player as usize].clone();
3015                    let mut tested = 0u32;
3016                    for &dealer_idx in &selected_dealers {
3017                        if round.bad(previous_successful_round.is_some(), dealer_idx) {
3018                            continue;
3019                        }
3020                        let dealer_pk = keys[dealer_idx as usize].public_key();
3021                        let dealer_log = dealer_logs
3022                            .get(&dealer_pk)
3023                            .unwrap_or_else(|| panic!("missing dealer log for {:?}", dealer_pk));
3024                        if dealer_log.get_ack(&player_pk).is_none() {
3025                            continue;
3026                        }
3027                        let replay = acked_dealings
3028                            .get(&player_pk)
3029                            .cloned()
3030                            .expect("player should be present");
3031                        let replay_without = replay
3032                            .into_iter()
3033                            .filter(|(dealer, _, _)| dealer != &dealer_pk);
3034                        let resume_logs: BTreeMap<_, _> = dealer_logs
3035                            .iter()
3036                            .filter(|(dealer, _)| *dealer != &dealer_pk)
3037                            .map(|(dealer, log)| (dealer.clone(), log.clone()))
3038                            .collect();
3039                        let (resumed, _) = Player::resume::<N3f1>(
3040                            info.clone(),
3041                            player_sk.clone(),
3042                            &resume_logs,
3043                            replay_without,
3044                        )
3045                        .expect("resume should succeed with stale logs");
3046                        let finalize_res = resumed.finalize::<N3f1, ed25519::Batch>(
3047                            &mut rng,
3048                            logs.clone(),
3049                            &Sequential,
3050                        );
3051                        assert!(
3052                            matches!(
3053                                finalize_res,
3054                                Err(FinalizeError::Error(Error::MissingPlayerDealing))
3055                            ),
3056                            "finalize without dealer {dealer_idx} message should return MissingPlayerDealing for player {i_player}"
3057                        );
3058                        tested += 1;
3059                    }
3060                    assert!(
3061                        tested > 0,
3062                        "finalize_missing_dealer_msg_fails for player {i_player} tested no dealers"
3063                    );
3064                }
3065
3066                // Compute expected reveals
3067                //
3068                // Note: We use union of no_acks and bad_shares since each (dealer, player) pair
3069                // results in at most one reveal in the protocol, regardless of whether the player
3070                // didn't ack, got a bad share, or both.
3071                let mut expected_reveals: BTreeMap<ed25519::PublicKey, u32> = BTreeMap::new();
3072                for &(dealer_idx, player_key_idx) in round.no_acks.union(&round.bad_shares) {
3073                    if !selected_dealers.contains(&dealer_idx) {
3074                        continue;
3075                    }
3076                    let pk = keys[player_key_idx as usize].public_key();
3077                    if selected_players.position(&pk).is_none() {
3078                        continue;
3079                    }
3080                    *expected_reveals.entry(pk).or_insert(0) += 1;
3081                }
3082
3083                // Verify each player's revealed status
3084                let reveal_threshold = info.reveal_threshold::<N3f1>();
3085                for player in player_set.iter() {
3086                    let expected =
3087                        expected_reveals.get(player).copied().unwrap_or(0) >= reveal_threshold;
3088                    let actual = observer_output.revealed().position(player).is_some();
3089                    assert_eq!(
3090                        expected, actual,
3091                        "Unexpected outcome for player {player:?} (expected={expected}, actual={actual})"
3092                    );
3093                }
3094
3095                // Finalize each player
3096                for (player_pk, player) in players.into_iter() {
3097                    let (player_output, share) = player
3098                        .finalize::<N3f1, ed25519::Batch>(&mut rng, logs.clone(), &Sequential)
3099                        .expect("Player finalize should succeed");
3100
3101                    assert_eq!(
3102                        player_output, observer_output,
3103                        "Player output should match observer output"
3104                    );
3105
3106                    // Verify share matches public polynomial
3107                    let expected_public = observer_output
3108                        .public
3109                        .partial_public(share.index)
3110                        .expect("share index should be valid");
3111                    let actual_public = share.public::<V>();
3112                    assert_eq!(
3113                        expected_public, actual_public,
3114                        "Share should match public polynomial"
3115                    );
3116
3117                    shares.insert(player_pk.clone(), share);
3118                }
3119
3120                // Initialize or verify threshold public key
3121                let current_public = *observer_output.public().public();
3122                match threshold_public_key {
3123                    None => threshold_public_key = Some(current_public),
3124                    Some(tpk) => {
3125                        assert_eq!(
3126                            tpk, current_public,
3127                            "Public key should remain constant across reshares"
3128                        );
3129                    }
3130                }
3131
3132                // Generate and verify threshold signature
3133                let test_message = format!("test message round {i_round}").into_bytes();
3134                let namespace = b"test";
3135
3136                let mut partial_sigs = Vec::new();
3137                for &i_player in &round.players {
3138                    let share = &shares[&keys[i_player as usize].public_key()];
3139                    let partial_sig = threshold::sign_message::<V>(share, namespace, &test_message);
3140
3141                    threshold::verify_message::<V>(
3142                        &observer_output.public,
3143                        namespace,
3144                        &test_message,
3145                        &partial_sig,
3146                    )
3147                    .expect("Partial signature verification should succeed");
3148
3149                    partial_sigs.push(partial_sig);
3150                }
3151
3152                let threshold = observer_output.quorum::<N3f1>();
3153                let threshold_sig = threshold::recover(
3154                    &observer_output.public,
3155                    &partial_sigs[0..threshold as usize],
3156                    &Sequential,
3157                )
3158                .expect("Should recover threshold signature");
3159
3160                // Verify against the saved public key
3161                ops::verify_message::<V>(
3162                    threshold_public_key.as_ref().unwrap(),
3163                    namespace,
3164                    &test_message,
3165                    &threshold_sig,
3166                )
3167                .expect("Threshold signature verification should succeed");
3168
3169                // Update state for next round
3170                previous_output = Some(observer_output);
3171            }
3172            Ok(())
3173        }
3174    }
3175
3176    #[cfg(feature = "arbitrary")]
3177    mod impl_arbitrary {
3178        use super::*;
3179        use arbitrary::{Arbitrary, Unstructured};
3180        use core::ops::ControlFlow;
3181
3182        const MAX_NUM_PARTICIPANTS: u32 = 20;
3183        const MAX_ROUNDS: u32 = 10;
3184
3185        fn arbitrary_masks<'a>(u: &mut Unstructured<'a>) -> arbitrary::Result<Masks> {
3186            Ok(Masks {
3187                info_summary: Arbitrary::arbitrary(u)?,
3188                dealer: Arbitrary::arbitrary(u)?,
3189                pub_msg: Arbitrary::arbitrary(u)?,
3190                log: Arbitrary::arbitrary(u)?,
3191            })
3192        }
3193
3194        /// Pick at most `num` elements at random from `data`, returning them.
3195        ///
3196        /// This needs mutable access to perform a shuffle.
3197        ///
3198        fn pick<'a, T>(
3199            u: &mut Unstructured<'a>,
3200            num: usize,
3201            mut data: Vec<T>,
3202        ) -> arbitrary::Result<Vec<T>> {
3203            let len = data.len();
3204            let num = num.min(len);
3205            // Invariant: 0..start is a random subset of data.
3206            for start in 0..num {
3207                data.swap(start, u.int_in_range(start..=len - 1)?);
3208            }
3209            data.truncate(num);
3210            Ok(data)
3211        }
3212
3213        fn arbitrary_round<'a>(
3214            u: &mut Unstructured<'a>,
3215            num_participants: u32,
3216            last_successful_players: Option<&Set<u32>>,
3217        ) -> arbitrary::Result<Round> {
3218            let dealers = if let Some(players) = last_successful_players {
3219                let to_pick = u.int_in_range(players.quorum::<N3f1>() as usize..=players.len())?;
3220                pick(u, to_pick, players.into_iter().copied().collect())?
3221            } else {
3222                let to_pick = u.int_in_range(1..=num_participants as usize)?;
3223                pick(u, to_pick, (0..num_participants).collect())?
3224            };
3225            let players = {
3226                let to_pick = u.int_in_range(1..=num_participants as usize)?;
3227                pick(u, to_pick, (0..num_participants).collect())?
3228            };
3229            let pairs = dealers
3230                .iter()
3231                .flat_map(|d| players.iter().map(|p| (*d, *p)))
3232                .collect::<Vec<_>>();
3233            let pick_pair_set = |u: &mut Unstructured<'a>| {
3234                let num = u.int_in_range(0..=pairs.len())?;
3235                if num == 0 {
3236                    return Ok(BTreeSet::new());
3237                }
3238                Ok(pick(u, num, pairs.clone())?.into_iter().collect())
3239            };
3240            let pick_dealer_set = |u: &mut Unstructured<'a>| {
3241                let num = u.int_in_range(0..=dealers.len())?;
3242                if num == 0 {
3243                    return Ok(BTreeSet::new());
3244                }
3245                Ok(pick(u, num, dealers.clone())?.into_iter().collect())
3246            };
3247            let round = Round {
3248                crash_resume_players: BTreeSet::new(),
3249                resume_missing_dealer_msg_fails: BTreeSet::new(),
3250                finalize_missing_dealer_msg_fails: BTreeSet::new(),
3251                no_acks: pick_pair_set(u)?,
3252                bad_shares: pick_pair_set(u)?,
3253                bad_player_sigs: {
3254                    let indices = pick_pair_set(u)?;
3255                    indices
3256                        .into_iter()
3257                        .map(|k| Ok((k, arbitrary_masks(u)?)))
3258                        .collect::<arbitrary::Result<_>>()?
3259                },
3260                bad_reveals: pick_pair_set(u)?,
3261                bad_dealer_sigs: {
3262                    let indices = pick_dealer_set(u)?;
3263                    indices
3264                        .into_iter()
3265                        .map(|k| Ok((k, arbitrary_masks(u)?)))
3266                        .collect::<arbitrary::Result<_>>()?
3267                },
3268                replace_shares: pick_dealer_set(u)?,
3269                shift_degrees: {
3270                    let indices = pick_dealer_set(u)?;
3271                    indices
3272                        .into_iter()
3273                        .map(|k| {
3274                            let expected = N3f1::quorum(players.len()) as i32 - 1;
3275                            let shift = u.int_in_range(1..=expected.max(1))?;
3276                            let shift = if bool::arbitrary(u)? { -shift } else { shift };
3277                            Ok((k, NonZeroI32::new(shift).expect("checked to not be zero")))
3278                        })
3279                        .collect::<arbitrary::Result<_>>()?
3280                },
3281                dealers,
3282                players,
3283            };
3284            Ok(round)
3285        }
3286
3287        impl<'a> Arbitrary<'a> for Plan {
3288            fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
3289                let num_participants = u.int_in_range(1..=MAX_NUM_PARTICIPANTS)?;
3290                let mut rounds = Vec::new();
3291                let mut last_successful_players: Option<Set<u32>> = None;
3292                u.arbitrary_loop(None, Some(MAX_ROUNDS), |u| {
3293                    let round =
3294                        arbitrary_round(u, num_participants, last_successful_players.as_ref())?;
3295                    if !round
3296                        .expect_failure(last_successful_players.as_ref().map(|x| x.len() as u32))
3297                    {
3298                        last_successful_players =
3299                            Some(round.players.iter().copied().try_collect().unwrap());
3300                    }
3301                    rounds.push(round);
3302                    Ok(ControlFlow::Continue(()))
3303                })?;
3304                let plan = Self {
3305                    num_participants: NZU32!(num_participants),
3306                    rounds,
3307                };
3308                plan.validate()
3309                    .map_err(|_| arbitrary::Error::IncorrectFormat)?;
3310                Ok(plan)
3311            }
3312        }
3313    }
3314}
3315
3316#[cfg(feature = "arbitrary")]
3317pub use test_plan::Plan as FuzzPlan;
3318
3319#[cfg(test)]
3320mod test {
3321    use super::{test_plan::*, *};
3322    use crate::{bls12381::primitives::variant::MinPk, ed25519};
3323    use anyhow::anyhow;
3324    use arbitrary::{Arbitrary, Unstructured};
3325    use commonware_invariants::minifuzz;
3326    use commonware_utils::{N3f1, TestRng, test_rng};
3327    use core::num::NonZeroI32;
3328
3329    const PRE_VERIFY_DEALERS: usize = 8;
3330    type PreVerifyLog = DealerLog<MinPk, ed25519::PublicKey>;
3331    type PreVerifyLogs = Logs<MinPk, ed25519::PublicKey, N3f1>;
3332
3333    fn check_pre_verify_log(
3334        info: &Info<MinPk, ed25519::PublicKey>,
3335        dealer: &ed25519::PublicKey,
3336        log: &PreVerifyLog,
3337    ) -> Result<DealerLogOutcome, DealerLogError> {
3338        let transcript = transcript_for_round(info);
3339        PreVerifyLogs::check_dealers::<ed25519::Batch>(
3340            &mut test_rng(),
3341            info,
3342            &Sequential,
3343            &transcript,
3344            &[(dealer, log)],
3345        )
3346        .pop()
3347        .expect("one dealer should produce one check")
3348        .1
3349    }
3350
3351    fn reshare_info(seed: u64) -> Info<MinPk, ed25519::PublicKey> {
3352        let participants = (seed..seed + 4)
3353            .map(ed25519::PrivateKey::from_seed)
3354            .map(|key| key.public_key())
3355            .try_collect::<Set<_>>()
3356            .expect("participants must be unique");
3357        let (previous, _) = deal::<MinPk, _, N3f1>(
3358            TestRng::new(seed + 4),
3359            Mode::NonZeroCounter,
3360            participants.clone(),
3361        )
3362        .expect("previous dealing must succeed");
3363        Info::new::<N3f1>(
3364            b"specific-reshare-error-test",
3365            1,
3366            Some(previous),
3367            Mode::NonZeroCounter,
3368            Reveal::V1,
3369            participants.clone(),
3370            participants,
3371        )
3372        .expect("reshare info must be valid")
3373    }
3374
3375    fn pub_msg_with_different_constant(
3376        info: &Info<MinPk, ed25519::PublicKey>,
3377        expected: &<MinPk as Variant>::Public,
3378        seed: u64,
3379    ) -> DealerPubMsg<MinPk> {
3380        let degree = info.degree::<N3f1>();
3381        let mut pub_msg = DealerPubMsg::<MinPk> {
3382            commitment: Poly::commit(Poly::new_with_constant(
3383                TestRng::new(seed),
3384                degree,
3385                Scalar::zero(),
3386            )),
3387        };
3388        if pub_msg.commitment.constant() == expected {
3389            pub_msg.commitment = Poly::commit(Poly::new_with_constant(
3390                TestRng::new(seed),
3391                degree,
3392                Scalar::one(),
3393            ));
3394        }
3395        assert_eq!(pub_msg.commitment.degree_exact(), degree);
3396        assert_ne!(pub_msg.commitment.constant(), expected);
3397        pub_msg
3398    }
3399
3400    #[test]
3401    fn info_rejects_inconsistent_previous_output() {
3402        let participants = (0..4)
3403            .map(ed25519::PrivateKey::from_seed)
3404            .map(|key| key.public_key())
3405            .try_collect::<Set<_>>()
3406            .expect("participants must be unique");
3407        let (previous, _) =
3408            deal::<MinPk, _, N3f1>(TestRng::new(4), Mode::NonZeroCounter, participants.clone())
3409                .expect("previous dealing must succeed");
3410
3411        let mut wrong_total = previous.clone();
3412        wrong_total.public = Sharing::<MinPk>::new(
3413            Mode::NonZeroCounter,
3414            NZU32!(1),
3415            Poly::commit(Poly::<Scalar>::new(TestRng::new(5), 2)),
3416        );
3417
3418        let mut excessive_degree = previous;
3419        excessive_degree.public = Sharing::<MinPk>::new(
3420            Mode::NonZeroCounter,
3421            NZU32!(4),
3422            Poly::commit(Poly::<Scalar>::new(TestRng::new(6), 3)),
3423        );
3424
3425        // Matching the participant count is insufficient when the recovery threshold does not
3426        // match the configured fault model.
3427        let mut lower_threshold = excessive_degree.clone();
3428        lower_threshold.public = Sharing::<MinPk>::new(
3429            Mode::NonZeroCounter,
3430            NZU32!(4),
3431            Poly::commit(Poly::<Scalar>::new(TestRng::new(7), 1)),
3432        );
3433        assert_eq!(lower_threshold.public.required(), 2);
3434        assert_eq!(lower_threshold.quorum::<N3f1>(), 3);
3435
3436        for previous in [wrong_total, excessive_degree, lower_threshold] {
3437            assert!(matches!(
3438                Info::<MinPk, _>::new::<N3f1>(
3439                    b"invalid-previous-output-test",
3440                    1,
3441                    Some(previous),
3442                    Mode::NonZeroCounter,
3443                    Reveal::V1,
3444                    participants.clone(),
3445                    participants.clone(),
3446                ),
3447                Err(Error::InvalidPreviousOutput)
3448            ));
3449        }
3450    }
3451
3452    fn is_revealed_after(
3453        reveal: Reveal,
3454        dealer_count: usize,
3455        player_count: usize,
3456        reveal_count: usize,
3457    ) -> bool {
3458        let keys: Vec<_> = (0..dealer_count + player_count)
3459            .map(|seed| ed25519::PrivateKey::from_seed(seed as u64 + 1_000))
3460            .collect();
3461        let dealer_keys = &keys[..dealer_count];
3462        let player_keys = &keys[dealer_count..];
3463        let dealers: Set<_> = dealer_keys
3464            .iter()
3465            .map(|key| key.public_key())
3466            .try_collect()
3467            .expect("dealers must be unique");
3468        let players: Set<_> = player_keys
3469            .iter()
3470            .map(|key| key.public_key())
3471            .try_collect()
3472            .expect("players must be unique");
3473        let info = Info::<MinPk, _>::new::<N3f1>(
3474            b"reveal-threshold-test",
3475            0,
3476            None,
3477            Mode::NonZeroCounter,
3478            reveal,
3479            dealers,
3480            players,
3481        )
3482        .expect("info must be valid");
3483        let target = player_keys[0].public_key();
3484        let player_keys: BTreeMap<_, _> = player_keys
3485            .iter()
3486            .cloned()
3487            .map(|key| (key.public_key(), key))
3488            .collect();
3489
3490        let required = usize::try_from(info.required_commitments::<N3f1>())
3491            .expect("required commitments exceed usize::MAX");
3492        let mut selected = Vec::new();
3493        for (dealer_number, dealer_key) in dealer_keys.iter().take(required).enumerate() {
3494            let (mut dealer, pub_msg, priv_msgs) = Dealer::start::<N3f1>(
3495                TestRng::new(dealer_number as u64),
3496                info.clone(),
3497                dealer_key.clone(),
3498                None,
3499            )
3500            .expect("dealer must start");
3501            for (player, priv_msg) in priv_msgs {
3502                if dealer_number < reveal_count && player == target {
3503                    continue;
3504                }
3505                let mut receiver = Player::<MinPk, _>::new(
3506                    info.clone(),
3507                    player_keys.get(&player).expect("player must exist").clone(),
3508                )
3509                .expect("player must initialize");
3510                let ack = receiver
3511                    .dealer_message::<N3f1>(dealer_key.public_key(), pub_msg.clone(), priv_msg)
3512                    .expect("dealing must be valid")
3513                    .expect("dealing must be new");
3514                dealer
3515                    .receive_player_ack(player, ack)
3516                    .expect("ack must be valid");
3517            }
3518            selected.push(
3519                dealer
3520                    .finalize::<N3f1>()
3521                    .check(&info)
3522                    .expect("dealer log must verify"),
3523            );
3524        }
3525        let selected = selected
3526            .into_iter()
3527            .try_collect::<Map<_, _>>()
3528            .expect("dealers must be unique");
3529        let output = Observe::reckon::<N3f1>(info, selected, &Sequential).output;
3530
3531        output.revealed().position(&target).is_some()
3532    }
3533
3534    #[test]
3535    fn reveal_threshold_uses_dealer_set_when_dealers_are_fewer() {
3536        assert!(is_revealed_after(Reveal::V1, 4, 10, 2));
3537    }
3538
3539    #[test]
3540    fn reveal_threshold_uses_dealer_set_when_dealers_are_more() {
3541        assert!(!is_revealed_after(Reveal::V1, 13, 4, 4));
3542    }
3543
3544    #[test]
3545    #[allow(deprecated)]
3546    fn legacy_reveal_threshold_uses_player_set() {
3547        assert!(!is_revealed_after(Reveal::V0, 4, 10, 2));
3548    }
3549
3550    #[test]
3551    #[allow(deprecated)]
3552    fn reveal_v1_is_committed_to_summary() {
3553        // Exercise both sharing modes so each non-legacy selector combination must
3554        // identify a distinct ceremony.
3555        let participants = (0..4)
3556            .map(ed25519::PrivateKey::from_seed)
3557            .map(|key| key.public_key())
3558            .try_collect::<Set<_>>()
3559            .expect("participants must be unique");
3560        let info = |mode, reveal| {
3561            Info::<MinPk, _>::new::<N3f1>(
3562                b"reveal-version-transcript-test",
3563                0,
3564                None,
3565                mode,
3566                reveal,
3567                participants.clone(),
3568                participants.clone(),
3569            )
3570            .expect("info must be valid")
3571        };
3572        let v0_non_zero = info(Mode::NonZeroCounter, Reveal::V0);
3573        let v0_roots = info(Mode::RootsOfUnity, Reveal::V0);
3574        let v1_non_zero = info(Mode::NonZeroCounter, Reveal::V1);
3575        let v1_roots = info(Mode::RootsOfUnity, Reveal::V1);
3576
3577        // Every selector combination identifies a distinct ceremony. The cross-pair
3578        // checks prevent the compact selector encodings from aliasing.
3579        assert_ne!(v0_non_zero.summary, v0_roots.summary);
3580        assert_ne!(v0_non_zero.summary, v1_non_zero.summary);
3581        assert_ne!(v0_non_zero.summary, v1_roots.summary);
3582        assert_ne!(v0_roots.summary, v1_non_zero.summary);
3583        assert_ne!(v0_roots.summary, v1_roots.summary);
3584        assert_ne!(v1_non_zero.summary, v1_roots.summary);
3585
3586        // Info identity follows the ceremony summary.
3587        assert_ne!(v0_non_zero, v1_non_zero);
3588        assert_ne!(v0_roots, v1_roots);
3589    }
3590
3591    #[test]
3592    #[allow(deprecated)]
3593    fn reveals_reject_cross_configured_logs() {
3594        // Hold every other ceremony input constant and vary only the revealed-share
3595        // calculation.
3596        let dealer = ed25519::PrivateKey::from_seed(0);
3597        let participants: Set<ed25519::PublicKey> = vec![dealer.public_key()]
3598            .try_into()
3599            .expect("participant must be unique");
3600        let info = |reveal| {
3601            Info::<MinPk, _>::new::<N3f1>(
3602                b"reveal-version-signature-test",
3603                0,
3604                None,
3605                Mode::NonZeroCounter,
3606                reveal,
3607                participants.clone(),
3608                participants.clone(),
3609            )
3610            .expect("info must be valid")
3611        };
3612        let v0 = info(Reveal::V0);
3613        let v1 = info(Reveal::V1);
3614
3615        // A V0 log remains valid in its ceremony but cannot enter a V1 ceremony.
3616        let (dealer, _, _) = Dealer::start::<N3f1>(TestRng::new(0), v0.clone(), dealer, None)
3617            .expect("dealer must start");
3618        let log = dealer.finalize::<N3f1>();
3619
3620        assert!(log.clone().check(&v0).is_some());
3621        assert!(log.check(&v1).is_none());
3622    }
3623
3624    #[test]
3625    #[allow(deprecated)]
3626    fn observers_match_players_for_each_reveal_calculation() {
3627        fn outputs(
3628            reveal: Reveal,
3629        ) -> (
3630            Output<MinPk, ed25519::PublicKey>,
3631            Output<MinPk, ed25519::PublicKey>,
3632            ed25519::PublicKey,
3633        ) {
3634            const DEALER_COUNT: usize = 4;
3635            const PLAYER_COUNT: usize = 10;
3636            const SELECTED_DEALERS: usize = 3;
3637            const TARGET_REVEALS: usize = 2;
3638
3639            // Two revealed dealings cross V1's dealer threshold but not V0's player
3640            // threshold, making the selected calculation observable in the output.
3641            let keys: Vec<_> = (0..DEALER_COUNT + PLAYER_COUNT)
3642                .map(|seed| ed25519::PrivateKey::from_seed(seed as u64 + 3_000))
3643                .collect();
3644            let dealer_keys = &keys[..DEALER_COUNT];
3645            let player_keys = &keys[DEALER_COUNT..];
3646            let dealers = dealer_keys
3647                .iter()
3648                .map(|key| key.public_key())
3649                .try_collect()
3650                .expect("dealers must be unique");
3651            let players = player_keys
3652                .iter()
3653                .map(|key| key.public_key())
3654                .try_collect()
3655                .expect("players must be unique");
3656            let info = Info::<MinPk, _>::new::<N3f1>(
3657                b"public-reveal-calculation-test",
3658                0,
3659                None,
3660                Mode::NonZeroCounter,
3661                reveal,
3662                dealers,
3663                players,
3664            )
3665            .expect("info must be valid");
3666            let target = player_keys[0].public_key();
3667            let finalizer_key = player_keys[1].clone();
3668            let finalizer = finalizer_key.public_key();
3669            let player_keys: BTreeMap<_, _> = player_keys
3670                .iter()
3671                .cloned()
3672                .map(|key| (key.public_key(), key))
3673                .collect();
3674            let mut verified_logs = BTreeMap::new();
3675            let mut logs = Logs::<MinPk, _, N3f1>::new(info.clone());
3676            let mut persisted = Vec::new();
3677
3678            // Build quorum logs while withholding the target's dealing from two dealers.
3679            // Retain the finalizer's dealings so its player path can resume from the same logs.
3680            for (dealer_number, dealer_key) in dealer_keys.iter().take(SELECTED_DEALERS).enumerate()
3681            {
3682                let dealer_pk = dealer_key.public_key();
3683                let (mut dealer, pub_msg, priv_msgs) = Dealer::start::<N3f1>(
3684                    TestRng::new(dealer_number as u64 + 4_000),
3685                    info.clone(),
3686                    dealer_key.clone(),
3687                    None,
3688                )
3689                .expect("dealer must start");
3690                for (player, priv_msg) in priv_msgs {
3691                    if dealer_number < TARGET_REVEALS && player == target {
3692                        continue;
3693                    }
3694                    if player == finalizer {
3695                        persisted.push((dealer_pk.clone(), pub_msg.clone(), priv_msg.clone()));
3696                    }
3697                    let mut receiver = Player::<MinPk, _>::new(
3698                        info.clone(),
3699                        player_keys.get(&player).expect("player must exist").clone(),
3700                    )
3701                    .expect("player must initialize");
3702                    let ack = receiver
3703                        .dealer_message::<N3f1>(dealer_pk.clone(), pub_msg.clone(), priv_msg)
3704                        .expect("dealing must be valid")
3705                        .expect("dealing must be new");
3706                    dealer
3707                        .receive_player_ack(player, ack)
3708                        .expect("ack must be valid");
3709                }
3710                let (dealer, log) = dealer
3711                    .finalize::<N3f1>()
3712                    .check(&info)
3713                    .expect("dealer log must verify");
3714                verified_logs.insert(dealer.clone(), log.clone());
3715                logs.record(dealer, log);
3716            }
3717
3718            // Run the selected logs through both the recovered-player and public-observer paths.
3719            let (player, _) =
3720                Player::resume::<N3f1>(info, finalizer_key, &verified_logs, persisted)
3721                    .expect("player must resume");
3722            let observed = observe::<MinPk, _, N3f1, ed25519::Batch>(
3723                &mut test_rng(),
3724                logs.clone(),
3725                &Sequential,
3726            )
3727            .expect("observation must succeed");
3728            let (finalized, _) = player
3729                .finalize::<N3f1, ed25519::Batch>(&mut test_rng(), logs, &Sequential)
3730                .expect("finalization must succeed");
3731            (finalized, observed, target)
3732        }
3733
3734        // Each calculation agrees across both paths, while target membership proves the
3735        // configured threshold rule changes the result.
3736        let (v1_finalized, v1_observed, v1_target) = outputs(Reveal::V1);
3737        let (v0_finalized, v0_observed, v0_target) = outputs(Reveal::V0);
3738
3739        assert_eq!(v1_finalized, v1_observed);
3740        assert_eq!(v0_finalized, v0_observed);
3741        assert!(v1_finalized.revealed().position(&v1_target).is_some());
3742        assert!(v0_finalized.revealed().position(&v0_target).is_none());
3743    }
3744
3745    #[test]
3746    fn reveal_threshold_uses_previous_players_during_reshare() {
3747        let keys: Vec<_> = (0..14)
3748            .map(|seed| ed25519::PrivateKey::from_seed(seed + 2_000))
3749            .collect();
3750        let previous_players = keys[..10]
3751            .iter()
3752            .map(|key| key.public_key())
3753            .try_collect::<Set<_>>()
3754            .expect("previous players must be unique");
3755        let (previous, _) = deal::<MinPk, _, N3f1>(
3756            TestRng::new(0),
3757            Mode::NonZeroCounter,
3758            previous_players.clone(),
3759        )
3760        .expect("previous dealing must succeed");
3761        let dealers = previous_players
3762            .iter()
3763            .take(7)
3764            .cloned()
3765            .try_collect::<Set<_>>()
3766            .expect("dealers must be unique");
3767        let players = keys[10..]
3768            .iter()
3769            .map(|key| key.public_key())
3770            .try_collect::<Set<_>>()
3771            .expect("players must be unique");
3772        let info = Info::new::<N3f1>(
3773            b"reshare-reveal-threshold-test",
3774            1,
3775            Some(previous),
3776            Mode::NonZeroCounter,
3777            Reveal::V1,
3778            dealers,
3779            players,
3780        )
3781        .expect("reshare info must be valid");
3782
3783        assert_eq!(info.required_commitments::<N3f1>(), 7);
3784        assert_eq!(info.reveal_threshold::<N3f1>(), 4);
3785    }
3786
3787    struct PreVerifyDealer {
3788        key: ed25519::PublicKey,
3789        valid: PreVerifyLog,
3790        invalid: PreVerifyLog,
3791    }
3792
3793    struct PreVerifyFixture {
3794        info: Info<MinPk, ed25519::PublicKey>,
3795        wrong_info: Info<MinPk, ed25519::PublicKey>,
3796        dealers: Vec<PreVerifyDealer>,
3797    }
3798
3799    impl PreVerifyFixture {
3800        fn new() -> Self {
3801            fn pre_verify_test_keys() -> Vec<ed25519::PrivateKey> {
3802                (0..PRE_VERIFY_DEALERS as u64)
3803                    .map(ed25519::PrivateKey::from_seed)
3804                    .collect()
3805            }
3806
3807            fn pre_verify_test_info(
3808                keys: &[ed25519::PrivateKey],
3809                round: u64,
3810            ) -> Info<MinPk, ed25519::PublicKey> {
3811                let dealers: Set<_> = keys
3812                    .iter()
3813                    .map(|sk| sk.public_key())
3814                    .try_collect()
3815                    .expect("dealers must be unique");
3816                Info::<MinPk, _>::new::<N3f1>(
3817                    b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
3818                    round,
3819                    None,
3820                    Mode::NonZeroCounter,
3821                    Reveal::V1,
3822                    dealers.clone(),
3823                    dealers,
3824                )
3825                .expect("info must be valid")
3826            }
3827
3828            fn generate_dealer_log(
3829                info: &Info<MinPk, ed25519::PublicKey>,
3830                keys: &[ed25519::PrivateKey],
3831                dealer_index: usize,
3832                seed: u64,
3833            ) -> DealerLog<MinPk, ed25519::PublicKey> {
3834                let mut players: BTreeMap<_, _> = keys
3835                    .iter()
3836                    .cloned()
3837                    .map(|sk| {
3838                        let pk = sk.public_key();
3839                        (
3840                            pk,
3841                            Player::<MinPk, _>::new(info.clone(), sk)
3842                                .expect("player initialization must succeed"),
3843                        )
3844                    })
3845                    .collect();
3846
3847                let dealer_sk = keys[dealer_index].clone();
3848                let dealer_pk = dealer_sk.public_key();
3849                let mut rng = TestRng::new(seed);
3850                let (mut dealer, pub_msg, priv_msgs) =
3851                    Dealer::start::<N3f1>(&mut rng, info.clone(), dealer_sk, None)
3852                        .expect("dealer initialization must succeed");
3853                for (player_pk, priv_msg) in priv_msgs {
3854                    let ack = players
3855                        .get_mut(&player_pk)
3856                        .expect("player should exist")
3857                        .dealer_message::<N3f1>(dealer_pk.clone(), pub_msg.clone(), priv_msg)
3858                        .expect("dealer message must succeed")
3859                        .expect("dealer message must be new");
3860                    dealer
3861                        .receive_player_ack(player_pk, ack)
3862                        .expect("ack handling must succeed");
3863                }
3864                dealer
3865                    .finalize::<N3f1>()
3866                    .check(info)
3867                    .expect("signed dealer log must verify against its own info")
3868                    .1
3869            }
3870
3871            let keys = pre_verify_test_keys();
3872            let info = pre_verify_test_info(&keys, 0);
3873            let wrong_info = pre_verify_test_info(&keys, 1);
3874            let mut logs_by_key: BTreeMap<_, _> = keys
3875                .iter()
3876                .enumerate()
3877                .map(|(dealer_index, sk)| {
3878                    let key = sk.public_key();
3879                    let seed = dealer_index as u64;
3880                    let valid = generate_dealer_log(&info, &keys, dealer_index, seed);
3881                    let invalid = generate_dealer_log(&wrong_info, &keys, dealer_index, seed);
3882                    assert_eq!(
3883                        valid.pub_msg, invalid.pub_msg,
3884                        "wrong-info log generation should only change transcript-bound signatures"
3885                    );
3886                    (key, (valid, invalid))
3887                })
3888                .collect();
3889            let dealers = info
3890                .dealers
3891                .iter()
3892                .cloned()
3893                .map(|key| {
3894                    let (valid, invalid) = logs_by_key
3895                        .remove(&key)
3896                        .expect("fixture should include every dealer");
3897                    PreVerifyDealer {
3898                        key,
3899                        valid,
3900                        invalid,
3901                    }
3902                })
3903                .collect();
3904            Self {
3905                info,
3906                wrong_info,
3907                dealers,
3908            }
3909        }
3910
3911        fn required_commitments(&self) -> usize {
3912            usize::try_from(self.info.required_commitments::<N3f1>())
3913                .expect("required commitments exceed usize::MAX")
3914        }
3915
3916        fn expected(&self, valid: &[bool]) -> Set<ed25519::PublicKey> {
3917            assert_eq!(
3918                valid.len(),
3919                self.dealers.len(),
3920                "fixture size should match case"
3921            );
3922            self.dealers
3923                .iter()
3924                .zip(valid.iter().copied())
3925                .filter(|(_, is_valid)| *is_valid)
3926                .take(self.required_commitments())
3927                .map(|(dealer, _)| dealer.key.clone())
3928                .try_collect()
3929                .expect("dealers must be unique")
3930        }
3931
3932        fn record(&self, logs: &mut PreVerifyLogs, dealer_index: usize, is_valid: bool) {
3933            let dealer = &self.dealers[dealer_index];
3934            let log = if is_valid {
3935                dealer.valid.clone()
3936            } else {
3937                dealer.invalid.clone()
3938            };
3939            logs.record(dealer.key.clone(), log);
3940        }
3941
3942        fn logs_for(
3943            &self,
3944            info: &Info<MinPk, ed25519::PublicKey>,
3945            valid: &[bool],
3946        ) -> PreVerifyLogs {
3947            assert_eq!(
3948                valid.len(),
3949                self.dealers.len(),
3950                "fixture size should match case"
3951            );
3952            let mut logs = PreVerifyLogs::new(info.clone());
3953            for (dealer_index, &is_valid) in valid.iter().enumerate() {
3954                self.record(&mut logs, dealer_index, is_valid);
3955            }
3956            logs
3957        }
3958    }
3959
3960    #[derive(Debug)]
3961    struct IncrementalPreVerifyCase {
3962        valid: [bool; PRE_VERIFY_DEALERS],
3963        batches: Vec<Vec<usize>>,
3964    }
3965
3966    impl<'a> Arbitrary<'a> for IncrementalPreVerifyCase {
3967        fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result<Self> {
3968            let mut valid = [false; PRE_VERIFY_DEALERS];
3969            for is_valid in &mut valid {
3970                *is_valid = u.arbitrary()?;
3971            }
3972
3973            let mut order: Vec<_> = (0..valid.len()).collect();
3974            for index in 0..valid.len() {
3975                let last = order.len() - 1;
3976                order.swap(index, u.int_in_range(index..=last)?);
3977            }
3978
3979            let mut batches = Vec::new();
3980            let mut start = 0;
3981            while start < order.len() {
3982                let batch_size = u.int_in_range(1..=order.len() - start)?;
3983                batches.push(order[start..start + batch_size].to_vec());
3984                start += batch_size;
3985            }
3986
3987            Ok(Self { valid, batches })
3988        }
3989    }
3990
3991    impl IncrementalPreVerifyCase {
3992        fn run(self, fixture: &PreVerifyFixture) -> arbitrary::Result<()> {
3993            let required_commitments = fixture.required_commitments();
3994            let expected = fixture.expected(&self.valid);
3995            let fresh = fixture.logs_for(&fixture.info, &self.valid);
3996
3997            let mut incremental = PreVerifyLogs::new(fixture.info.clone());
3998            let mut incremental_rng = test_rng();
3999            for batch in &self.batches {
4000                for &dealer_index in batch {
4001                    fixture.record(&mut incremental, dealer_index, self.valid[dealer_index]);
4002                }
4003                incremental.pre_verify::<ed25519::Batch>(&mut incremental_rng, &Sequential);
4004            }
4005
4006            let mut fresh_rng = test_rng();
4007            let fresh_selected = fresh
4008                .select::<ed25519::Batch>(&mut fresh_rng, &Sequential)
4009                .map(|(_, selection)| selection.keys().clone());
4010            let incremental_selected = incremental
4011                .select::<ed25519::Batch>(&mut incremental_rng, &Sequential)
4012                .map(|(_, selection)| selection.keys().clone());
4013
4014            match &fresh_selected {
4015                Ok(selected) => assert_eq!(
4016                    selected, &expected,
4017                    "all-at-once selection disagreed with validity mask: {:?}",
4018                    self
4019                ),
4020                Err(_) => assert!(
4021                    expected.len() < required_commitments,
4022                    "all-at-once selection failed despite quorum-sized expected set: {:?}",
4023                    self
4024                ),
4025            }
4026
4027            match (fresh_selected, incremental_selected) {
4028                (Err(_), Err(_)) => {}
4029                (Ok(fresh_selected), Ok(incremental_selected)) => assert_eq!(
4030                    incremental_selected, fresh_selected,
4031                    "incremental selection disagreed with all-at-once selection: {:?}",
4032                    self
4033                ),
4034                (Ok(fresh_selected), Err(err)) => panic!(
4035                    "incremental selection failed with {err:?} but all-at-once selected {fresh_selected:?}: {self:?}"
4036                ),
4037                (Err(err), Ok(incremental_selected)) => panic!(
4038                    "incremental selection returned {incremental_selected:?} but all-at-once failed with {err:?}: {self:?}"
4039                ),
4040            }
4041
4042            Ok(())
4043        }
4044    }
4045
4046    #[test]
4047    fn incremental_pre_verify_preserves_dealer_order() {
4048        let fixture = PreVerifyFixture::new();
4049        minifuzz::test(move |u| u.arbitrary::<IncrementalPreVerifyCase>()?.run(&fixture));
4050    }
4051
4052    #[test]
4053    fn check_dealers_reports_invalid_reveal() {
4054        let fixture = PreVerifyFixture::new();
4055        let dealer = &fixture.dealers[0];
4056        let mut log = dealer.valid.clone();
4057        let DealerResult::Ok(results) = &mut log.results else {
4058            panic!("valid fixture should contain player results");
4059        };
4060        let player = fixture.info.players.iter().next().unwrap().clone();
4061        *results.get_value_mut(&player).unwrap() =
4062            AckOrReveal::Reveal(DealerPrivMsg::new(Scalar::zero()));
4063
4064        let result = check_pre_verify_log(&fixture.info, &dealer.key, &log);
4065        assert!(matches!(
4066            result,
4067            Err(DealerLogError::Fault(FaultReason::InvalidReveal))
4068        ));
4069    }
4070
4071    #[test]
4072    fn dealer_log_identifies_unexpected_dealer() {
4073        let fixture = PreVerifyFixture::new();
4074        let dealer = &fixture.dealers[0];
4075        let stranger = ed25519::PrivateKey::from_seed(u64::MAX).public_key();
4076        assert!(fixture.info.dealer_index(&stranger).is_none());
4077        assert!(matches!(
4078            check_pre_verify_log(&fixture.info, &stranger, &dealer.valid),
4079            Err(DealerLogError::UnexpectedDealer)
4080        ));
4081    }
4082
4083    #[test]
4084    fn dealer_public_message_distinguishes_fault_reasons() {
4085        let fixture = PreVerifyFixture::new();
4086        let dealer = &fixture.dealers[0];
4087        let expected = fixture.info.degree::<N3f1>();
4088        let actual = 0;
4089        assert_ne!(expected, actual);
4090        let log = DealerLog {
4091            pub_msg: DealerPubMsg::<MinPk> {
4092                commitment: Poly::commit(Poly::new_with_constant(
4093                    TestRng::new(10_000),
4094                    actual,
4095                    Scalar::one(),
4096                )),
4097            },
4098            results: dealer.valid.results.clone(),
4099        };
4100        assert!(matches!(
4101            check_pre_verify_log(&fixture.info, &dealer.key, &log),
4102            Err(DealerLogError::Fault(
4103                FaultReason::InvalidCommitmentDegree {
4104                    expected: error_expected,
4105                    actual: error_actual,
4106                }
4107            )) if error_expected == expected && error_actual == actual
4108        ));
4109
4110        let info = reshare_info(20_000);
4111        let dealer = info.dealers.iter().next().unwrap().clone();
4112        let expected = info
4113            .previous
4114            .as_ref()
4115            .unwrap()
4116            .share_commitment(&dealer)
4117            .unwrap();
4118        let pub_msg = pub_msg_with_different_constant(&info, &expected, 20_005);
4119        let log = DealerLog {
4120            pub_msg,
4121            results: DealerResult::TooManyReveals,
4122        };
4123        assert!(matches!(
4124            check_pre_verify_log(&info, &dealer, &log),
4125            Err(DealerLogError::Fault(
4126                FaultReason::MismatchedReshareCommitment
4127            ))
4128        ));
4129    }
4130
4131    #[test]
4132    fn dealer_public_message_distinguishes_live_errors() -> anyhow::Result<()> {
4133        // Initial-round validation preserves the exact degree mismatch.
4134        let fixture = PreVerifyFixture::new();
4135        let dealer = fixture.dealers[0].key.clone();
4136        let expected = fixture.info.degree::<N3f1>();
4137        let actual = 0;
4138        assert_ne!(expected, actual);
4139        let pub_msg = DealerPubMsg::<MinPk> {
4140            commitment: Poly::commit(Poly::new_with_constant(
4141                TestRng::new(10_000),
4142                actual,
4143                Scalar::one(),
4144            )),
4145        };
4146        let mut player = Player::new(fixture.info, ed25519::PrivateKey::from_seed(0))?;
4147        assert!(matches!(
4148            player.dealer_message::<N3f1>(
4149                dealer,
4150                pub_msg,
4151                DealerPrivMsg::new(Scalar::zero()),
4152            ),
4153            Err(DealerMessageError::InvalidCommitmentDegree {
4154                expected: error_expected,
4155                actual: error_actual,
4156            }) if error_expected == expected && error_actual == actual
4157        ));
4158
4159        // Reshare validation preserves the previous-share mismatch.
4160        let info = reshare_info(20_000);
4161        let dealer = info.dealers.iter().next().unwrap().clone();
4162        let expected = info
4163            .previous
4164            .as_ref()
4165            .unwrap()
4166            .share_commitment(&dealer)
4167            .unwrap();
4168        let pub_msg = pub_msg_with_different_constant(&info, &expected, 20_005);
4169        let mut player = Player::new(info, ed25519::PrivateKey::from_seed(20_000))?;
4170        assert!(matches!(
4171            player.dealer_message::<N3f1>(dealer, pub_msg, DealerPrivMsg::new(Scalar::zero()),),
4172            Err(DealerMessageError::MismatchedReshareCommitment)
4173        ));
4174
4175        Ok(())
4176    }
4177
4178    #[test]
4179    fn dealer_log_distinguishes_unavailable_and_faulty_outcomes() {
4180        let fixture = PreVerifyFixture::new();
4181        let dealer = &fixture.dealers[0];
4182
4183        assert!(matches!(
4184            check_pre_verify_log(&fixture.info, &dealer.key, &dealer.valid),
4185            Ok(DealerLogOutcome::Available)
4186        ));
4187
4188        let mut log = dealer.valid.clone();
4189        let DealerResult::Ok(results) = &mut log.results else {
4190            panic!("valid fixture should contain player results");
4191        };
4192        results.truncate(results.len() - 1);
4193        assert!(matches!(
4194            check_pre_verify_log(&fixture.info, &dealer.key, &log),
4195            Err(DealerLogError::Fault(FaultReason::MismatchedLogPlayers))
4196        ));
4197
4198        let mut log = dealer.valid.clone();
4199        log.results = DealerResult::TooManyReveals;
4200        assert!(matches!(
4201            check_pre_verify_log(&fixture.info, &dealer.key, &log),
4202            Ok(DealerLogOutcome::Unavailable)
4203        ));
4204
4205        let mut log = dealer.valid.clone();
4206        let DealerResult::Ok(results) = &mut log.results else {
4207            panic!("valid fixture should contain player results");
4208        };
4209        let excessive_reveals = usize::try_from(fixture.info.max_reveals::<N3f1>())
4210            .expect("maximum reveals exceed usize::MAX")
4211            + 1;
4212        for result in results.values_mut().iter_mut().take(excessive_reveals) {
4213            *result = AckOrReveal::Reveal(DealerPrivMsg::new(Scalar::one()));
4214        }
4215        assert!(matches!(
4216            check_pre_verify_log(&fixture.info, &dealer.key, &log),
4217            Err(DealerLogError::Fault(FaultReason::ExcessiveReveals))
4218        ));
4219    }
4220
4221    #[test]
4222    fn logs_are_bound_to_constructor_info() {
4223        let fixture = PreVerifyFixture::new();
4224        let mut logs = fixture.logs_for(&fixture.info, &[false; PRE_VERIFY_DEALERS]);
4225        let mut wrong_logs = fixture.logs_for(&fixture.wrong_info, &[false; PRE_VERIFY_DEALERS]);
4226        let mut rng = test_rng();
4227
4228        logs.pre_verify::<ed25519::Batch>(&mut rng, &Sequential);
4229        wrong_logs.pre_verify::<ed25519::Batch>(&mut rng, &Sequential);
4230        assert!(
4231            wrong_logs
4232                .select::<ed25519::Batch>(&mut rng, &Sequential)
4233                .is_ok(),
4234            "control check: logs should verify when bound to the round they were created for"
4235        );
4236        let Err(Failure::InsufficientLogs {
4237            required,
4238            found,
4239            faults,
4240            unavailable,
4241        }) = observe::<MinPk, _, N3f1, ed25519::Batch>(&mut rng, logs, &Sequential)
4242        else {
4243            panic!("logs bound to a different round must fail reconciliation");
4244        };
4245        assert_eq!(
4246            usize::try_from(required).expect("required commitments exceed usize::MAX"),
4247            fixture.required_commitments()
4248        );
4249        assert_eq!(found, 0);
4250        assert_eq!(faults.len(), PRE_VERIFY_DEALERS);
4251        assert!(unavailable.is_empty());
4252        for dealer in &fixture.dealers {
4253            assert!(matches!(
4254                faults.get_value(&dealer.key),
4255                Some(FaultReason::InvalidAck)
4256            ));
4257        }
4258    }
4259
4260    #[test]
4261    fn pre_verify_preserves_classifications_and_invalidates_replacements() {
4262        let fixture = PreVerifyFixture::new();
4263        let mut logs = fixture.logs_for(&fixture.info, &[true; PRE_VERIFY_DEALERS]);
4264        fixture.record(&mut logs, 0, false);
4265
4266        let mut too_many_reveals = fixture.dealers[1].valid.clone();
4267        too_many_reveals.results = DealerResult::TooManyReveals;
4268        logs.record(fixture.dealers[1].key.clone(), too_many_reveals);
4269
4270        let mut wrong_players = fixture.dealers[2].valid.clone();
4271        let DealerResult::Ok(results) = &mut wrong_players.results else {
4272            panic!("valid fixture should contain player results");
4273        };
4274        results.truncate(results.len() - 1);
4275        logs.record(fixture.dealers[2].key.clone(), wrong_players);
4276        logs.pre_verify::<ed25519::Batch>(&mut test_rng(), &Sequential);
4277
4278        let Err(Failure::InsufficientLogs {
4279            required,
4280            found,
4281            faults,
4282            unavailable,
4283        }) = observe::<MinPk, _, N3f1, ed25519::Batch>(&mut test_rng(), logs.clone(), &Sequential)
4284        else {
4285            panic!("three rejected logs must prevent reconciliation");
4286        };
4287        assert_eq!(
4288            usize::try_from(required).expect("required commitments exceed usize::MAX"),
4289            fixture.required_commitments()
4290        );
4291        assert_eq!(
4292            usize::try_from(found).expect("valid dealer count exceeds usize::MAX"),
4293            PRE_VERIFY_DEALERS - 3
4294        );
4295        assert_eq!(faults.len(), 2);
4296        assert!(matches!(
4297            faults.get_value(&fixture.dealers[0].key),
4298            Some(FaultReason::InvalidAck)
4299        ));
4300        assert_eq!(unavailable.len(), 1);
4301        assert!(unavailable.position(&fixture.dealers[1].key).is_some());
4302        assert!(faults.get_value(&fixture.dealers[1].key).is_none());
4303        assert!(matches!(
4304            faults.get_value(&fixture.dealers[2].key),
4305            Some(FaultReason::MismatchedLogPlayers)
4306        ));
4307
4308        fixture.record(&mut logs, 0, true);
4309        assert!(
4310            observe::<MinPk, _, N3f1, ed25519::Batch>(&mut test_rng(), logs, &Sequential).is_ok(),
4311            "replacing one rejected log must restore the exact quorum"
4312        );
4313    }
4314
4315    #[test]
4316    fn missing_logs_report_dealers_as_unavailable() {
4317        let fixture = PreVerifyFixture::new();
4318        let logs = PreVerifyLogs::new(fixture.info.clone());
4319        let Err(Failure::InsufficientLogs {
4320            required,
4321            found,
4322            faults,
4323            unavailable,
4324        }) = observe::<MinPk, _, N3f1, ed25519::Batch>(&mut test_rng(), logs, &Sequential)
4325        else {
4326            panic!("missing logs must fail reconciliation");
4327        };
4328        assert_eq!(
4329            usize::try_from(required).expect("required commitments exceed usize::MAX"),
4330            fixture.required_commitments()
4331        );
4332        assert_eq!(found, 0);
4333        assert!(faults.is_empty());
4334        assert_eq!(unavailable, fixture.info.dealers);
4335    }
4336
4337    #[test]
4338    fn finalize_wraps_reconciliation_failure() {
4339        let fixture = PreVerifyFixture::new();
4340        let player =
4341            Player::<MinPk, _>::new(fixture.info.clone(), ed25519::PrivateKey::from_seed(0))
4342                .expect("player initialization must succeed");
4343        let logs = PreVerifyLogs::new(fixture.info);
4344
4345        assert!(matches!(
4346            player.finalize::<N3f1, ed25519::Batch>(&mut test_rng(), logs, &Sequential),
4347            Err(FinalizeError::Failure(Failure::InsufficientLogs { .. }))
4348        ));
4349    }
4350
4351    #[test]
4352    fn finalize_rejects_logs_bound_to_different_round() {
4353        let fixture = PreVerifyFixture::new();
4354        let player =
4355            Player::<MinPk, _>::new(fixture.info.clone(), ed25519::PrivateKey::from_seed(0))
4356                .expect("player initialization must succeed");
4357        let wrong_logs = fixture.logs_for(&fixture.wrong_info, &[false; PRE_VERIFY_DEALERS]);
4358
4359        let result =
4360            player.finalize::<N3f1, ed25519::Batch>(&mut test_rng(), wrong_logs, &Sequential);
4361
4362        assert!(
4363            matches!(result, Err(FinalizeError::Error(Error::MismatchedLogs))),
4364            "finalize should reject logs bound to a different round"
4365        );
4366    }
4367
4368    fn replaced_dealing_fixture(
4369        replacement_ack: bool,
4370    ) -> (
4371        Player<MinPk, ed25519::PrivateKey>,
4372        Logs<MinPk, ed25519::PublicKey, N3f1>,
4373    ) {
4374        let mut keys: Vec<_> = (0..4).map(ed25519::PrivateKey::from_seed).collect();
4375        keys.sort_by_key(|key| key.public_key());
4376        let participants: Set<_> = keys
4377            .iter()
4378            .map(|key| key.public_key())
4379            .try_collect()
4380            .expect("participants must be unique");
4381        let info = Info::<MinPk, _>::new::<N3f1>(
4382            b"stale-dealing-test",
4383            0,
4384            None,
4385            Mode::NonZeroCounter,
4386            Reveal::V1,
4387            participants.clone(),
4388            participants,
4389        )
4390        .expect("info must be valid");
4391        let malicious = keys[0].clone();
4392        let malicious_pk = malicious.public_key();
4393        let target_key = keys[3].clone();
4394        let target = target_key.public_key();
4395
4396        let (_, stale_pub_msg, stale_priv_msgs) =
4397            Dealer::start::<N3f1>(TestRng::new(100), info.clone(), malicious, None)
4398                .expect("dealer must start");
4399        let stale_priv_msg = stale_priv_msgs
4400            .into_iter()
4401            .find_map(|(player, priv_msg)| (player == target).then_some(priv_msg))
4402            .expect("target dealing must exist");
4403
4404        let mut verified_logs = BTreeMap::new();
4405        let mut logs = Logs::<MinPk, _, N3f1>::new(info.clone());
4406        let mut persisted = vec![(malicious_pk, stale_pub_msg.clone(), stale_priv_msg)];
4407        for (dealer_number, dealer_key) in keys.iter().take(3).enumerate() {
4408            let seed = 200 + dealer_number as u64;
4409            let dealer_pk = dealer_key.public_key();
4410            let (mut dealer, pub_msg, priv_msgs) =
4411                Dealer::start::<N3f1>(TestRng::new(seed), info.clone(), dealer_key.clone(), None)
4412                    .expect("dealer must start");
4413            if dealer_number == 0 {
4414                assert_ne!(pub_msg, stale_pub_msg);
4415            }
4416
4417            for (player, priv_msg) in priv_msgs {
4418                if dealer_number == 0 && player == target && !replacement_ack {
4419                    continue;
4420                }
4421                if dealer_number != 0 && player == target {
4422                    persisted.push((dealer_pk.clone(), pub_msg.clone(), priv_msg.clone()));
4423                }
4424                let receiver_key = keys
4425                    .iter()
4426                    .find(|key| key.public_key() == player)
4427                    .expect("receiver must exist")
4428                    .clone();
4429                let mut receiver = Player::<MinPk, _>::new(info.clone(), receiver_key)
4430                    .expect("player must initialize");
4431                let ack = receiver
4432                    .dealer_message::<N3f1>(dealer_pk.clone(), pub_msg.clone(), priv_msg)
4433                    .expect("dealing must be valid")
4434                    .expect("dealing must be new");
4435                dealer
4436                    .receive_player_ack(player, ack)
4437                    .expect("ack must be valid");
4438            }
4439
4440            let (dealer, log) = dealer
4441                .finalize::<N3f1>()
4442                .check(&info)
4443                .expect("dealer log must verify");
4444            if dealer_number == 0 {
4445                assert_eq!(log.get_reveal(&target).is_some(), !replacement_ack);
4446            }
4447            verified_logs.insert(dealer.clone(), log.clone());
4448            logs.record(dealer, log);
4449        }
4450
4451        let (target_player, _) =
4452            Player::resume::<N3f1>(info, target_key, &verified_logs, persisted)
4453                .expect("target must resume");
4454        (target_player, logs)
4455    }
4456
4457    #[test]
4458    fn stale_dealing_is_not_reused_for_replaced_log() {
4459        let (target_player, logs) = replaced_dealing_fixture(false);
4460        let (output, share) = target_player
4461            .finalize::<N3f1, ed25519::Batch>(&mut test_rng(), logs, &Sequential)
4462            .expect("target must finalize");
4463        assert_eq!(
4464            share.public::<MinPk>(),
4465            output
4466                .public()
4467                .partial_public(share.index)
4468                .expect("share index must be valid")
4469        );
4470    }
4471
4472    #[test]
4473    fn replacement_ack_rejects_stale_persisted_dealing() {
4474        let (target_player, logs) = replaced_dealing_fixture(true);
4475        assert!(matches!(
4476            target_player.finalize::<N3f1, ed25519::Batch>(&mut test_rng(), logs, &Sequential),
4477            Err(FinalizeError::Error(Error::InvalidPersistedDealing { .. }))
4478        ));
4479    }
4480
4481    #[test]
4482    fn single_round() -> anyhow::Result<()> {
4483        Plan::new(NZU32!(4))
4484            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
4485            .run::<MinPk>(0)
4486    }
4487
4488    #[test]
4489    fn multiple_rounds() -> anyhow::Result<()> {
4490        Plan::new(NZU32!(4))
4491            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
4492            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
4493            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
4494            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
4495            .run::<MinPk>(0)
4496    }
4497
4498    #[test]
4499    fn player_crash_resume_after_dealer() -> anyhow::Result<()> {
4500        Plan::new(NZU32!(4))
4501            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).crash_resume_player(1, 2))
4502            .run::<MinPk>(0)
4503    }
4504
4505    #[test]
4506    fn resume_missing_good_dealer_message_fails_after_checkpoint() -> anyhow::Result<()> {
4507        Plan::new(NZU32!(4))
4508            .with(
4509                Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4510                    .resume_missing_dealer_msg_fails(2, 1),
4511            )
4512            .run::<MinPk>(0)
4513    }
4514
4515    #[test]
4516    fn resume_missing_good_dealer_message_skips_unacked_players() -> anyhow::Result<()> {
4517        Plan::new(NZU32!(4))
4518            .with(
4519                Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4520                    .no_ack(1, 0)
4521                    .resume_missing_dealer_msg_fails(2, 1),
4522            )
4523            .run::<MinPk>(0)
4524    }
4525
4526    #[test]
4527    fn finalize_fails_after_resume_without_good_dealer_message() -> anyhow::Result<()> {
4528        Plan::new(NZU32!(4))
4529            .with(
4530                Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4531                    .no_ack(0, 1)
4532                    .finalize_missing_dealer_msg_fails(0),
4533            )
4534            .run::<MinPk>(0)
4535    }
4536
4537    #[test]
4538    fn invalid_checkpoint_configs_fail_validation() {
4539        assert!(
4540            Plan::new(NZU32!(4))
4541                .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).crash_resume_player(4, 2))
4542                .validate()
4543                .is_err()
4544        );
4545        assert!(
4546            Plan::new(NZU32!(4))
4547                .with(
4548                    Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4549                        .resume_missing_dealer_msg_fails(1, 2),
4550                )
4551                .validate()
4552                .is_err()
4553        );
4554        assert!(
4555            Plan::new(NZU32!(4))
4556                .with(
4557                    Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4558                        .bad_reveal(1, 0)
4559                        .resume_missing_dealer_msg_fails(2, 1),
4560                )
4561                .validate()
4562                .is_err()
4563        );
4564    }
4565
4566    #[test]
4567    fn changing_committee() -> anyhow::Result<()> {
4568        Plan::new(NonZeroU32::new(5).unwrap())
4569            .with(Round::new(vec![0, 1, 2], vec![1, 2, 3]))
4570            .with(Round::new(vec![1, 2, 3], vec![2, 3, 4]))
4571            .with(Round::new(vec![2, 3, 4], vec![3, 4, 0]))
4572            .with(Round::new(vec![3, 4, 0], vec![4, 0, 1]))
4573            .run::<MinPk>(0)
4574    }
4575
4576    #[test]
4577    fn missing_ack() -> anyhow::Result<()> {
4578        // With 4 players, max_faults = 1, so 1 missing ack per dealer is OK
4579        Plan::new(NonZeroU32::new(4).unwrap())
4580            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 0))
4581            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 1))
4582            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 2))
4583            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).no_ack(0, 3))
4584            .run::<MinPk>(0)
4585    }
4586
4587    #[test]
4588    fn increasing_decreasing_committee() -> anyhow::Result<()> {
4589        Plan::new(NonZeroU32::new(5).unwrap())
4590            .with(Round::new(vec![0, 1], vec![0, 1, 2]))
4591            .with(Round::new(vec![0, 1, 2], vec![0, 1, 2, 3]))
4592            .with(Round::new(vec![0, 1, 2], vec![0, 1]))
4593            .with(Round::new(vec![0, 1], vec![0, 1, 2, 3, 4]))
4594            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1]))
4595            .run::<MinPk>(0)
4596    }
4597
4598    #[test]
4599    fn bad_reveal_fails() -> anyhow::Result<()> {
4600        Plan::new(NonZeroU32::new(4).unwrap())
4601            .with(Round::new(vec![0], vec![0, 1, 2, 3]).bad_reveal(0, 1))
4602            .run::<MinPk>(0)
4603    }
4604
4605    #[test]
4606    fn bad_share() -> anyhow::Result<()> {
4607        Plan::new(NonZeroU32::new(4).unwrap())
4608            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).bad_share(0, 1))
4609            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).bad_share(0, 2))
4610            .run::<MinPk>(0)
4611    }
4612
4613    #[test]
4614    fn shift_degree_fails() -> anyhow::Result<()> {
4615        Plan::new(NonZeroU32::new(4).unwrap())
4616            .with(Round::new(vec![0], vec![0, 1, 2, 3]).shift_degree(
4617                0,
4618                NonZeroI32::new(1).ok_or_else(|| anyhow!("invalid NZI32"))?,
4619            ))
4620            .run::<MinPk>(0)
4621    }
4622
4623    #[test]
4624    fn replace_share_fails() -> anyhow::Result<()> {
4625        Plan::new(NonZeroU32::new(4).unwrap())
4626            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]))
4627            .with(Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3]).replace_share(0))
4628            .run::<MinPk>(0)
4629    }
4630
4631    #[test]
4632    fn too_many_reveals_dealer() -> anyhow::Result<()> {
4633        Plan::new(NonZeroU32::new(4).unwrap())
4634            .with(
4635                Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4636                    .no_ack(0, 0)
4637                    .no_ack(0, 1),
4638            )
4639            .run::<MinPk>(0)
4640    }
4641
4642    #[test]
4643    fn too_many_reveals_player() -> anyhow::Result<()> {
4644        Plan::new(NonZeroU32::new(4).unwrap())
4645            .with(
4646                Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4647                    .no_ack(0, 0)
4648                    .no_ack(1, 0)
4649                    .no_ack(3, 0),
4650            )
4651            .run::<MinPk>(0)
4652    }
4653
4654    #[test]
4655    fn bad_sigs() -> anyhow::Result<()> {
4656        Plan::new(NonZeroU32::new(4).unwrap())
4657            .with(
4658                Round::new(vec![0, 1, 2, 3], vec![0, 1, 2, 3])
4659                    .bad_dealer_sig(
4660                        0,
4661                        Masks {
4662                            log: vec![0xFF; 8],
4663                            ..Default::default()
4664                        },
4665                    )
4666                    .bad_player_sig(
4667                        0,
4668                        1,
4669                        Masks {
4670                            pub_msg: vec![0xFF; 8],
4671                            ..Default::default()
4672                        },
4673                    ),
4674            )
4675            .run::<MinPk>(0)
4676    }
4677
4678    #[test]
4679    fn issue_2745_regression() -> anyhow::Result<()> {
4680        Plan::new(NonZeroU32::new(6).unwrap())
4681            .with(
4682                Round::new(vec![0], vec![5, 1, 3, 0, 4])
4683                    .no_ack(0, 5)
4684                    .bad_share(0, 5),
4685            )
4686            .with(Round::new(vec![0, 1, 3, 4], vec![0]))
4687            .with(Round::new(vec![0], vec![0]))
4688            .run::<MinPk>(0)
4689    }
4690
4691    #[test]
4692    fn signed_dealer_log_commitment() -> anyhow::Result<()> {
4693        let sk = ed25519::PrivateKey::from_seed(0);
4694        let pk = sk.public_key();
4695        let info = Info::<MinPk, _>::new::<N3f1>(
4696            b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
4697            0,
4698            None,
4699            Mode::NonZeroCounter,
4700            Reveal::V1,
4701            vec![sk.public_key()].try_into().unwrap(),
4702            vec![sk.public_key()].try_into().unwrap(),
4703        )?;
4704        let mut log0 = {
4705            let (dealer, _, _) =
4706                Dealer::start::<N3f1>(&mut test_rng(), info.clone(), sk.clone(), None)?;
4707            dealer.finalize::<N3f1>()
4708        };
4709        let mut log1 = {
4710            let (mut dealer, pub_msg, priv_msgs) =
4711                Dealer::start::<N3f1>(&mut test_rng(), info.clone(), sk.clone(), None)?;
4712            let mut player = Player::new(info.clone(), sk)?;
4713            let ack = player
4714                .dealer_message::<N3f1>(pk.clone(), pub_msg, priv_msgs[0].1.clone())?
4715                .expect("dealer message must be new");
4716            dealer.receive_player_ack(pk, ack)?;
4717            dealer.finalize::<N3f1>()
4718        };
4719        std::mem::swap(&mut log0.log, &mut log1.log);
4720        assert!(log0.check(&info).is_none());
4721        assert!(log1.check(&info).is_none());
4722
4723        Ok(())
4724    }
4725
4726    #[test]
4727    fn dealer_message_reports_errors_and_skips_duplicates() -> anyhow::Result<()> {
4728        let a = ed25519::PrivateKey::from_seed(0);
4729        let b = ed25519::PrivateKey::from_seed(1);
4730        let stranger = ed25519::PrivateKey::from_seed(2);
4731        let (a_pk, b_pk, stranger_pk) = (a.public_key(), b.public_key(), stranger.public_key());
4732        let members: Set<ed25519::PublicKey> = vec![a_pk.clone(), b_pk.clone()].try_into().unwrap();
4733        let info = Info::<MinPk, _>::new::<N3f1>(
4734            b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
4735            0,
4736            None,
4737            Mode::NonZeroCounter,
4738            Reveal::V1,
4739            members.clone(),
4740            members,
4741        )?;
4742
4743        // Dealer A produces one private message per player.
4744        let (_, pub_msg, priv_msgs) =
4745            Dealer::start::<N3f1>(&mut test_rng(), info.clone(), a, None)?;
4746        let priv_for_a = priv_msgs
4747            .iter()
4748            .find(|(player, _)| *player == a_pk)
4749            .map(|(_, msg)| msg.clone())
4750            .expect("share for a");
4751        let priv_for_b = priv_msgs
4752            .iter()
4753            .find(|(player, _)| *player == b_pk)
4754            .map(|(_, msg)| msg.clone())
4755            .expect("share for b");
4756
4757        // Persisted invalid input is reported as local state corruption.
4758        assert!(matches!(
4759            Player::resume::<N3f1>(
4760                info.clone(),
4761                b.clone(),
4762                &BTreeMap::new(),
4763                [(a_pk.clone(), pub_msg.clone(), priv_for_a.clone())],
4764            ),
4765            Err(Error::InvalidPersistedDealing { .. })
4766        ));
4767        let mut player = Player::new(info, b)?;
4768        assert!(matches!(
4769            player.dealer_message::<N3f1>(a_pk.clone(), pub_msg.clone(), priv_for_a),
4770            Err(DealerMessageError::InvalidDealerShare)
4771        ));
4772
4773        // Live message errors expose the rejection reason without attributing it.
4774        assert!(matches!(
4775            player.dealer_message::<N3f1>(stranger_pk, pub_msg.clone(), priv_for_b.clone(),),
4776            Err(DealerMessageError::UnexpectedDealer)
4777        ));
4778
4779        // A correct dealing validates.
4780        assert!(
4781            player
4782                .dealer_message::<N3f1>(a_pk.clone(), pub_msg.clone(), priv_for_b.clone())?
4783                .is_some()
4784        );
4785
4786        // Replaying a dealer's message is a benign skip.
4787        assert!(matches!(
4788            player.dealer_message::<N3f1>(a_pk, pub_msg, priv_for_b),
4789            Ok(None)
4790        ));
4791
4792        Ok(())
4793    }
4794
4795    #[test]
4796    fn receive_player_ack_reports_non_attributable_errors() -> anyhow::Result<()> {
4797        let a = ed25519::PrivateKey::from_seed(0);
4798        let stranger = ed25519::PrivateKey::from_seed(1);
4799        let (a_pk, stranger_pk) = (a.public_key(), stranger.public_key());
4800        let members: Set<ed25519::PublicKey> = vec![a_pk.clone()].try_into().unwrap();
4801        let info = Info::<MinPk, _>::new::<N3f1>(
4802            b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
4803            0,
4804            None,
4805            Mode::NonZeroCounter,
4806            Reveal::V1,
4807            members.clone(),
4808            members,
4809        )?;
4810
4811        let (mut dealer, pub_msg, priv_msgs) =
4812            Dealer::start::<N3f1>(TestRng::new(0), info.clone(), a.clone(), None)?;
4813        let (_, alternate_pub_msg, alternate_priv_msgs) =
4814            Dealer::start::<N3f1>(TestRng::new(1), info.clone(), a.clone(), None)?;
4815        assert_ne!(pub_msg, alternate_pub_msg);
4816
4817        let mut player = Player::new(info.clone(), a.clone())?;
4818        let ack = player
4819            .dealer_message::<N3f1>(a_pk.clone(), pub_msg, priv_msgs[0].1.clone())?
4820            .expect("valid ack");
4821
4822        // An honest player can acknowledge an alternate public message from an
4823        // equivocating dealer. A signature mismatch therefore cannot identify a
4824        // faulty player.
4825        let mut alternate_player = Player::new(info, a)?;
4826        let alternate_ack = alternate_player
4827            .dealer_message::<N3f1>(
4828                a_pk.clone(),
4829                alternate_pub_msg,
4830                alternate_priv_msgs[0].1.clone(),
4831            )?
4832            .expect("valid ack for alternate message");
4833        assert!(matches!(
4834            dealer.receive_player_ack(a_pk.clone(), alternate_ack),
4835            Err(PlayerAckError::InvalidAck)
4836        ));
4837
4838        // Out-of-round identities produce the precise non-attributed error.
4839        assert!(matches!(
4840            dealer.receive_player_ack(stranger_pk, ack.clone()),
4841            Err(PlayerAckError::UnexpectedPlayer)
4842        ));
4843
4844        // The genuine ack is still accepted.
4845        assert!(dealer.receive_player_ack(a_pk, ack).is_ok());
4846
4847        Ok(())
4848    }
4849
4850    #[test]
4851    fn info_with_different_mode_is_not_equal() -> Result<(), Error> {
4852        let sk = ed25519::PrivateKey::from_seed(0);
4853        let pk = sk.public_key();
4854        let dealers: Set<ed25519::PublicKey> = vec![pk.clone()].try_into().unwrap();
4855        let players: Set<ed25519::PublicKey> = vec![pk].try_into().unwrap();
4856
4857        let non_zero_counter_info = Info::<MinPk, _>::new::<N3f1>(
4858            b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
4859            0,
4860            None,
4861            Mode::NonZeroCounter,
4862            Reveal::V1,
4863            dealers.clone(),
4864            players.clone(),
4865        )?;
4866        let roots_of_unity_mode_info = Info::<MinPk, _>::new::<N3f1>(
4867            b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
4868            0,
4869            None,
4870            Mode::RootsOfUnity,
4871            Reveal::V1,
4872            dealers,
4873            players,
4874        )?;
4875
4876        assert_ne!(non_zero_counter_info, roots_of_unity_mode_info);
4877        Ok(())
4878    }
4879
4880    #[test]
4881    fn resume_ignores_invalid_logged_ack_signature() -> Result<(), Error> {
4882        let dealer_sk = ed25519::PrivateKey::from_seed(11);
4883        let dealer_pk = dealer_sk.public_key();
4884        let player_sk = ed25519::PrivateKey::from_seed(22);
4885        let player_pk = player_sk.public_key();
4886        let dealers: Set<ed25519::PublicKey> = vec![dealer_pk.clone()].try_into().unwrap();
4887        let players: Set<ed25519::PublicKey> = vec![player_pk.clone()].try_into().unwrap();
4888        let info = Info::<MinPk, _>::new::<N3f1>(
4889            b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
4890            0,
4891            None,
4892            Mode::NonZeroCounter,
4893            Reveal::V1,
4894            dealers.clone(),
4895            players.clone(),
4896        )?;
4897        let wrong_round_info = Info::<MinPk, _>::new::<N3f1>(
4898            b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_TEST",
4899            1,
4900            None,
4901            Mode::NonZeroCounter,
4902            Reveal::V1,
4903            dealers,
4904            players,
4905        )?;
4906        let (_, pub_msg, _) =
4907            Dealer::start::<N3f1>(&mut test_rng(), info.clone(), dealer_sk, None)?;
4908        let bad_ack = PlayerAck {
4909            sig: transcript_for_ack(
4910                &transcript_for_round(&wrong_round_info),
4911                &dealer_pk,
4912                &pub_msg,
4913            )
4914            .sign(&player_sk),
4915        };
4916        let results: Map<_, _> = vec![(player_pk, AckOrReveal::Ack(bad_ack))]
4917            .into_iter()
4918            .try_collect()
4919            .unwrap();
4920        let mut logs = BTreeMap::new();
4921        logs.insert(
4922            dealer_pk,
4923            DealerLog {
4924                pub_msg,
4925                results: DealerResult::Ok(results),
4926            },
4927        );
4928
4929        let resumed = Player::resume::<N3f1>(info, player_sk, &logs, []);
4930        assert!(resumed.is_ok());
4931        let (_, acks) = resumed.unwrap();
4932        assert!(acks.is_empty());
4933
4934        Ok(())
4935    }
4936
4937    #[test]
4938    fn test_dealer_priv_msg_redacted() {
4939        let mut rng = test_rng();
4940        let msg = DealerPrivMsg::new(Scalar::random(&mut rng));
4941        let debug = format!("{:?}", msg);
4942        assert!(debug.contains("REDACTED"));
4943    }
4944
4945    #[test]
4946    fn test_dealer_priv_msg_decode_rejects_zero_scalar() {
4947        let mut encoded = Scalar::zero().encode();
4948        let decoded = DealerPrivMsg::read_cfg(&mut encoded, &());
4949        assert!(decoded.is_err());
4950    }
4951
4952    #[test]
4953    fn test_dealer_pub_msg_decode_rejects_zero_commitment() {
4954        let mut rng = test_rng();
4955        let commitment = Poly::commit(Poly::new_with_constant(&mut rng, 0, Scalar::zero()));
4956        let mut encoded = DealerPubMsg::<MinPk> { commitment }.encode();
4957        let decoded = DealerPubMsg::<MinPk>::read_cfg(&mut encoded, &NZU32!(1));
4958        assert!(decoded.is_err());
4959    }
4960
4961    #[cfg(feature = "arbitrary")]
4962    mod conformance {
4963        use super::*;
4964        use commonware_codec::conformance::CodecConformance;
4965        use commonware_conformance::Conformance;
4966
4967        fn feldman_transcript(seed: u64, mode: Mode, reveal: Reveal) -> Vec<u8> {
4968            const APPLICATION_NAMESPACE: &[u8] =
4969                b"_COMMONWARE_CRYPTOGRAPHY_BLS12381_DKG_CONFORMANCE";
4970            let dealer_sk = ed25519::PrivateKey::from_seed(11);
4971            let dealer_pk = dealer_sk.public_key();
4972            let player_sk = ed25519::PrivateKey::from_seed(22);
4973            let player_pk = player_sk.public_key();
4974            let dealers: Set<ed25519::PublicKey> = vec![dealer_pk.clone()].try_into().unwrap();
4975            let players: Set<ed25519::PublicKey> = vec![player_pk].try_into().unwrap();
4976            let info = Info::<MinPk, _>::new::<N3f1>(
4977                APPLICATION_NAMESPACE,
4978                seed,
4979                None,
4980                mode,
4981                reveal,
4982                dealers,
4983                players,
4984            )
4985            .unwrap();
4986
4987            let (_, pub_msg, _) = Dealer::start::<N3f1>(
4988                &mut TestRng::new(seed),
4989                info.clone(),
4990                dealer_sk.clone(),
4991                None,
4992            )
4993            .unwrap();
4994
4995            let round_transcript = transcript_for_round(&info);
4996            let ack = transcript_for_ack(&round_transcript, &dealer_pk, &pub_msg);
4997            let ack_summary = ack.summarize();
4998            let ack_signature = ack.sign(&player_sk);
4999
5000            let log = DealerLog {
5001                pub_msg,
5002                results: DealerResult::TooManyReveals,
5003            };
5004            let log_summary = transcript_for_log(&info, &log).summarize();
5005            let signed_log = SignedDealerLog::sign(&dealer_sk, &info, log);
5006
5007            let mut output = info.summary.encode().to_vec();
5008            output.extend(ack_summary.encode());
5009            output.extend(ack_signature.encode());
5010            output.extend(log_summary.encode());
5011            output.extend(signed_log.encode());
5012            output
5013        }
5014
5015        /// Pins the full transcript for one polynomial/reveal mode tag pair.
5016        struct FeldmanTranscript<const POLYNOMIAL_MODE: u8, const REVEAL_MODE: u8>;
5017
5018        impl<const POLYNOMIAL_MODE: u8, const REVEAL_MODE: u8> Conformance
5019            for FeldmanTranscript<POLYNOMIAL_MODE, REVEAL_MODE>
5020        {
5021            #[allow(deprecated)]
5022            async fn commit(seed: u64) -> Vec<u8> {
5023                let (mode, reveal) = match (POLYNOMIAL_MODE, REVEAL_MODE) {
5024                    (0, 0) => (Mode::NonZeroCounter, Reveal::V0),
5025                    (0, 1) => (Mode::NonZeroCounter, Reveal::V1),
5026                    (1, 0) => (Mode::RootsOfUnity, Reveal::V0),
5027                    (1, 1) => (Mode::RootsOfUnity, Reveal::V1),
5028                    _ => panic!("unsupported Feldman transcript mode"),
5029                };
5030                feldman_transcript(seed, mode, reveal)
5031            }
5032        }
5033
5034        commonware_conformance::conformance_tests! {
5035            FeldmanTranscript<0, 0> => 16,
5036            FeldmanTranscript<0, 1> => 16,
5037            FeldmanTranscript<1, 0> => 16,
5038            FeldmanTranscript<1, 1> => 16,
5039            CodecConformance<Output<MinPk, ed25519::PublicKey>>,
5040            CodecConformance<DealerPubMsg<MinPk>>,
5041            CodecConformance<DealerPrivMsg>,
5042            CodecConformance<PlayerAck<ed25519::PublicKey>>,
5043            CodecConformance<AckOrReveal<ed25519::PublicKey>>,
5044            CodecConformance<DealerResult<ed25519::PublicKey>>,
5045            CodecConformance<DealerLog<MinPk, ed25519::PublicKey>>,
5046            CodecConformance<SignedDealerLog<MinPk, ed25519::PrivateKey>>,
5047        }
5048    }
5049}