Skip to main content

commonware_consensus/simplex/
elector.rs

1//! Leader election strategies for simplex consensus.
2//!
3//! This module provides the [`Config`] and [`Elector`] traits for customizing
4//! how leaders are selected for each consensus round, along with built-in implementations.
5//!
6//! # Built-in Electors
7//!
8//! - [`RoundRobin`]/[`RoundRobinElector`]: Deterministic rotation through participants
9//!   based on view number. Optionally shuffled using a seed. Works with any signing scheme.
10//!
11//! - [`Random`]/[`RandomElector`]: Uses randomness derived from BLS threshold VRF signatures
12//!   for unpredictable leader selection. Falls back to round-robin for the first view
13//!   (no certificate available). Requires [`super::scheme::bls12381_threshold::vrf`]
14//!   (implements [`super::scheme::bls12381_threshold::vrf::Seedable`]).
15//!
16//! # Custom Electors
17//!
18//! Applications can implement [`Config`] and [`Elector`] for custom leader
19//! selection logic such as stake-weighted selection or other application-specific strategies.
20//!
21//! Each elector declares a [`Mode`]. A [`Scheduled`] elector selects every leader from a schedule
22//! fixed when it is built, so consensus can elect a term's incoming leader before the term's
23//! unlocking certificate exists, as [pipelined handoffs](crate::simplex#pipelined-handoff)
24//! require. A [`Dynamic`] elector derives leaders from that certificate and never pipelines
25//! handoffs.
26//!
27//! Declare `type Mode = Dynamic;` to receive `Option<&S::Certificate>` in `elect`, or
28//! `type Mode = Scheduled;` to receive `()`. Generic callers convert the certificate with
29//! [`Mode::input`].
30//!
31//! # Usage
32//!
33//! Users configure leader election with an elector [`Config`] (for example,
34//! [`RoundRobin`]) and pass it to the consensus configuration. Consensus builds
35//! the initialized [`Elector`] with the scheme participants before starting.
36
37use crate::{
38    simplex::scheme::bls12381_threshold::vrf as bls12381_threshold_vrf,
39    types::{Participant, Round, TermLength, View, ViewDelta},
40};
41use commonware_codec::Encode;
42use commonware_cryptography::{
43    Hasher, PublicKey, Sha256,
44    bls12381::primitives::variant::Variant,
45    certificate::{Scheme, Verifier},
46};
47use commonware_utils::{Widen, modulo, ordered::Set};
48use std::{fmt, marker::PhantomData, time::Duration};
49
50/// Configuration for creating an [`Elector`].
51///
52/// Users create and configure this type, then pass it to the consensus configuration.
53/// Consensus will call [`build`](Config::build) internally with the correct
54/// participant set to create the initialized [`Elector`].
55///
56/// # Determinism Requirement
57///
58/// Implementations **must** be deterministic. Honest participants with the same
59/// configuration and participant set must select the same leader for each round.
60/// This is stronger than returning the same output for identical inputs because
61/// honest participants may call [`Elector::elect`] with different certificates for
62/// the same round. See [`Elector`] for the certificate handling requirements.
63pub trait Config<S: Scheme>: Clone + Send + 'static {
64    /// The initialized elector type.
65    type Elector: Elector<S>;
66
67    /// Builds the elector with the given participants.
68    ///
69    /// Called internally by consensus with the correct participant set.
70    ///
71    /// # Panics
72    ///
73    /// Implementations should panic if `participants` is empty.
74    fn build(self, participants: &Set<S::PublicKey>) -> Self::Elector;
75}
76
77/// Leadership term structure reported by an [`Elector`].
78#[derive(Clone, Copy, Debug, PartialEq, Eq)]
79pub struct Terms {
80    /// Number of consecutive views per term (one if and only if rotating).
81    length: TermLength,
82    /// Term-abandonment timeout (set if and only if `length` exceeds one).
83    stall_timeout: Option<Duration>,
84    /// Optimistic intra-term lookahead (zero unless `length` exceeds one).
85    optimistic_views: ViewDelta,
86}
87
88impl Terms {
89    /// Every view is its own term: a new leader is elected each view, and
90    /// leader rotation itself bounds how long finality can stall.
91    pub const fn rotating() -> Self {
92        Self {
93            length: TermLength::ONE,
94            stall_timeout: None,
95            optimistic_views: ViewDelta::zero(),
96        }
97    }
98
99    /// Views are grouped into terms of `length` consecutive views served by
100    /// one leader.
101    ///
102    /// The length is consensus-critical, so every participant must configure
103    /// the same value (see [`TermLength`]).
104    ///
105    /// `stall_timeout` is local policy. It is the maximum time an entered
106    /// view may remain unfinalized before this participant abandons the
107    /// term. On expiry it treats its current view as timed out and votes
108    /// nullify, which (with a quorum) forms a nullification covering the
109    /// rest of the term and evicts the leader.
110    ///
111    /// A Byzantine stable leader can keep every per-view timer satisfied
112    /// while preventing finality, since each view notarizes and certifies
113    /// but no finalization certificate forms. With single-view terms, leader
114    /// rotation bounds such a stall to one view. With longer terms, this
115    /// timeout bounds it instead.
116    ///
117    /// `optimistic_views` is how far a participant may optimistically run
118    /// ahead of certified ancestry within a term. Zero disables optimistic
119    /// validation entirely, and values wider than `length` are accepted but
120    /// capped by the windows themselves. The voter tracks a round for every
121    /// optimistic view and for every peer proposal it admits ahead of them, so
122    /// memory scales with the smaller of about twice `optimistic_views` and
123    /// `length`. See [Optimistic Validation] for the exact windows. Like the
124    /// stall timeout, this is local policy, so mismatched values across
125    /// participants only degrade the optimization, never safety.
126    ///
127    /// [Optimistic Validation]: crate::simplex#optimistic-validation
128    ///
129    /// # Panics
130    ///
131    /// Panics if `length` is 1 or if `stall_timeout` is zero. Single-view
132    /// terms are [`Terms::rotating`] (the default), where per-view timeouts
133    /// already bound a stall and no optimistic window exists.
134    pub const fn stable(
135        length: TermLength,
136        stall_timeout: Duration,
137        optimistic_views: ViewDelta,
138    ) -> Self {
139        assert!(
140            length.get() > 1,
141            "stable leaders require a term length greater than 1"
142        );
143        assert!(
144            !stall_timeout.is_zero(),
145            "stable leaders require a stall timeout greater than zero"
146        );
147        Self {
148            length,
149            stall_timeout: Some(stall_timeout),
150            optimistic_views,
151        }
152    }
153
154    /// Returns the number of consecutive views per term.
155    ///
156    /// Returns [`TermLength::ONE`] if and only if this is [`Terms::rotating`].
157    /// A length of one is the definition of rotation, not an approximation of
158    /// it: all term arithmetic ([`View::covers`], [`View::admits`],
159    /// [`View::term_index`], [`View::next_term_start`]) reduces exactly to
160    /// per-view behavior at length one. The only regime fact the length does
161    /// not carry is the stall deadline, which callers read from
162    /// [`Terms::stall_timeout`].
163    pub const fn length(&self) -> TermLength {
164        self.length
165    }
166
167    /// Returns the term-abandonment timeout, if stable leaders are configured.
168    ///
169    /// Returns `Some` if and only if [`Self::length`] is greater than one.
170    pub const fn stall_timeout(&self) -> Option<Duration> {
171        self.stall_timeout
172    }
173
174    /// Returns the optimistic intra-term lookahead (see [`Terms::stable`]).
175    ///
176    /// Always zero when [`Self::length`] is one.
177    pub const fn optimistic_views(&self) -> ViewDelta {
178        self.optimistic_views
179    }
180}
181
182impl Default for Terms {
183    fn default() -> Self {
184        Self::rotating()
185    }
186}
187
188mod private {
189    pub trait Sealed {}
190}
191
192/// How an [`Elector`] uses the certificate that unlocks a round.
193///
194/// This trait is sealed to [`Scheduled`] and [`Dynamic`]. External types can implement
195/// [`Elector`] and choose either mode.
196pub trait Mode: private::Sealed {
197    /// What [`Elector::elect`] receives in place of the unlocking certificate.
198    type Input<'a, C: 'a>;
199
200    /// Converts the unlocking certificate, `None` only for view 1, into the input.
201    fn input<C>(certificate: Option<&C>) -> Self::Input<'_, C>;
202
203    /// Returns the input available before the unlocking certificate exists, or `None` when
204    /// election must wait for it.
205    fn early<'a, C: 'a>() -> Option<Self::Input<'a, C>>;
206}
207
208/// [`Mode`] of an elector whose leader schedule is fixed when it is built.
209///
210/// [`Elector::elect`] receives `()`, so consensus can also elect a term's incoming leader
211/// before the certificate that unlocks the term exists. It may then call [`Elector::elect`]
212/// several times per view, so keep election inexpensive.
213pub struct Scheduled;
214
215/// [`Mode`] of an elector that derives leaders from the certificate that unlocks a round.
216///
217/// [`Elector::elect`] receives that certificate, `None` only for view 1. Consensus elects
218/// only once the certificate exists, so these electors never pipeline handoffs.
219pub struct Dynamic;
220
221impl private::Sealed for Scheduled {}
222impl private::Sealed for Dynamic {}
223
224impl Mode for Scheduled {
225    type Input<'a, C: 'a> = ();
226
227    fn input<C>(_: Option<&C>) {}
228
229    fn early<'a, C: 'a>() -> Option<()> {
230        Some(())
231    }
232}
233
234impl Mode for Dynamic {
235    type Input<'a, C: 'a> = Option<&'a C>;
236
237    fn input<C>(certificate: Option<&C>) -> Option<&C> {
238        certificate
239    }
240
241    fn early<'a, C: 'a>() -> Option<Option<&'a C>> {
242        None
243    }
244}
245
246/// The input that elector `E`'s [`Mode`] supplies to [`Elector::elect`].
247pub type Input<'a, S, E> =
248    <<E as Elector<S>>::Mode as Mode>::Input<'a, <S as Verifier>::Certificate>;
249
250/// An initialized elector that can select leaders for consensus rounds.
251///
252/// Consensus obtains initialized electors from [`Config::build`] so leader
253/// election and term arithmetic use the same participant set.
254///
255/// # Certificate Handling
256///
257/// A [`Dynamic`] elector receives the certificate that unlocked the target view, or `None`
258/// for view 1 (the first view after genesis). With stable leaders, a nullification
259/// certificate can skip to the next term start, so this is not necessarily a certificate
260/// from the immediately previous view. A [`Scheduled`] elector receives `()` instead.
261///
262/// Whether certificate data is safe to use for leader selection depends on the
263/// certificate scheme. Certificates are not necessarily canonical: schemes that
264/// retain signer contributions can produce different valid certificates for the
265/// same subject from different quorum subsets. Message reordering or a Byzantine
266/// participant can therefore cause honest participants to call `elect` for the
267/// same round with different certificate values. Implementations must not derive
268/// the leader from a certificate's raw encoding or signer set unless the scheme
269/// guarantees that the result is invariant across every valid representation.
270///
271/// Honest participants may also enter the same round with certificates for
272/// different subjects (for example, one via a notarization of the previous view and
273/// another via a nullification). With `term_length > 1`, those certificates may
274/// even be from different views. Implementations must return the same leader for
275/// every certificate that can unlock the round. [`RoundRobinElector`] is
276/// [`Scheduled`], so it never receives a certificate. [`RandomElector`] uses the
277/// recovered threshold seed signature, which is independent of vote type and quorum
278/// subset for a given round. [`Random`] does not support `term_length > 1` because
279/// certificates from different views carry different seed signatures.
280pub trait Elector<S: Scheme>: Clone + Send + 'static {
281    /// Whether [`Self::elect`] derives leaders from the unlocking certificate ([`Dynamic`])
282    /// or from a schedule fixed when the elector is built ([`Scheduled`]).
283    type Mode: Mode;
284
285    /// Returns the leadership term structure this elector was built with.
286    ///
287    /// Callers that need term arithmetic should use this value so leader
288    /// election and protocol term handling stay aligned.
289    fn terms(&self) -> Terms;
290
291    /// Selects the leader for the given round.
292    ///
293    /// This method **must** be a pure function given the elector's initialization state.
294    ///
295    /// Implementations **must** return the same leader for every view within a
296    /// stable-leader term (as defined by [`Self::terms`]): nullification
297    /// coverage, finalize gating, and leader-inactivity tracking all assume the
298    /// leader is constant for the remainder of a term. This contract is not
299    /// enforced at runtime: once a round's leader is set, consensus never
300    /// replaces it, even if it consults the elector again for that round. A
301    /// non-conforming implementation leaves participants with inconsistent
302    /// leaders and stalls progress.
303    ///
304    /// See [Certificate Handling](Elector#certificate-handling) for `input`.
305    ///
306    /// Returns the index of the selected leader in the participants list.
307    fn elect(&self, round: Round, input: Input<'_, S, Self>) -> Participant;
308}
309
310/// Configuration for round-robin leader election.
311///
312/// Rotates through participants based on `(epoch + term) % num_participants`, where `term` is the
313/// stable-leader term containing the view.
314/// The rotation order can be shuffled at construction using a seed.
315///
316/// Works with any signing scheme.
317#[derive(Debug, Default)]
318pub struct RoundRobin<H: Hasher = Sha256> {
319    seed: Option<Vec<u8>>,
320    terms: Terms,
321    _phantom: PhantomData<H>,
322}
323
324impl<H: Hasher> Clone for RoundRobin<H> {
325    fn clone(&self) -> Self {
326        Self {
327            seed: self.seed.clone(),
328            terms: self.terms,
329            _phantom: PhantomData,
330        }
331    }
332}
333
334impl<H: Hasher> RoundRobin<H> {
335    /// Creates a round-robin config that will shuffle the rotation order based on seed.
336    ///
337    /// The seed is used during [`Config::build`] to deterministically
338    /// shuffle the permutation.
339    pub fn shuffled(seed: &[u8]) -> Self {
340        Self {
341            seed: Some(seed.to_vec()),
342            terms: Terms::rotating(),
343            _phantom: PhantomData,
344        }
345    }
346
347    /// Enables stable leaders: `term_length` consecutive views share a leader,
348    /// a term abandoned after `stall_timeout` evicts them, and participants
349    /// may run up to `optimistic_views` ahead within a term (see
350    /// [`Terms::stable`]).
351    ///
352    /// The term length is consensus-critical: every participant must configure
353    /// the same value (see [`TermLength`]). The timeout and lookahead are
354    /// local policy.
355    ///
356    /// # Panics
357    ///
358    /// Panics if `term_length` is 1 or `stall_timeout` is zero (see
359    /// [`Terms::stable`]).
360    pub const fn with_term(
361        mut self,
362        term_length: TermLength,
363        stall_timeout: Duration,
364        optimistic_views: ViewDelta,
365    ) -> Self {
366        self.terms = Terms::stable(term_length, stall_timeout, optimistic_views);
367        self
368    }
369}
370
371impl<S: Scheme, H: Hasher> Config<S> for RoundRobin<H> {
372    type Elector = RoundRobinElector<S>;
373
374    fn build(self, participants: &Set<S::PublicKey>) -> RoundRobinElector<S> {
375        assert!(!participants.is_empty(), "no participants");
376
377        let mut permutation: Vec<Participant> = (0..participants.len())
378            .map(Participant::from_usize)
379            .collect();
380
381        if let Some(seed) = &self.seed {
382            permutation.sort_by_cached_key(|&index| H::hash(&[seed, &index.get().encode()]));
383        }
384
385        RoundRobinElector {
386            permutation,
387            terms: self.terms,
388            _phantom: PhantomData,
389        }
390    }
391}
392
393/// Initialized round-robin leader elector.
394///
395/// Created via [`RoundRobin::build`].
396#[derive(Clone, Debug)]
397pub struct RoundRobinElector<S: Scheme> {
398    permutation: Vec<Participant>,
399    terms: Terms,
400    _phantom: PhantomData<S>,
401}
402
403impl<S: Scheme> Elector<S> for RoundRobinElector<S> {
404    type Mode = Scheduled;
405
406    fn terms(&self) -> Terms {
407        self.terms
408    }
409
410    fn elect(&self, round: Round, _: ()) -> Participant {
411        // In order to get a stable leader, use the 1-based index of the term
412        let term_idx = round.view().term_index(self.terms.length());
413
414        // Incorporate the epoch number
415        let n: u64 = Widen::widen(self.permutation.len());
416        let idx = round.epoch().get().wrapping_add(term_idx) % n;
417        let idx = usize::try_from(idx).expect("leader index fits in usize");
418        self.permutation[idx]
419    }
420}
421
422/// Signature-to-leader mapping used by [`Random`].
423#[derive(Clone, Copy, Debug, PartialEq, Eq)]
424pub enum RandomVersion {
425    /// Maps the encoded threshold signature directly to a participant.
426    #[deprecated(
427        note = "mapping encoded threshold signature directly to participants can bias selection"
428    )]
429    V0,
430    /// Hashes the encoded threshold signature before mapping it to a participant.
431    ///
432    /// The hasher is selected by [`Random`]'s `H` type parameter and defaults to [`Sha256`].
433    V1,
434}
435
436/// Configuration for leader election using threshold signature randomness.
437///
438/// Uses the seed signature from BLS threshold certificates to derive unpredictable
439/// leader selection. Falls back to standard round-robin for view 1 when no
440/// certificate is available.
441///
442/// This elector does not support stable leaders: it has no term-length
443/// configuration and [`Elector::terms`] always returns [`Terms::rotating`].
444///
445/// Only works with [`super::scheme::bls12381_threshold::vrf`]
446/// (implements [`super::scheme::bls12381_threshold::vrf::Seedable`]).
447pub struct Random<H: Hasher = Sha256> {
448    version: RandomVersion,
449    _hasher: PhantomData<H>,
450}
451
452impl<H: Hasher> Random<H> {
453    /// Creates a configuration with the specified signature-to-leader mapping.
454    pub const fn new(version: RandomVersion) -> Self {
455        Self {
456            version,
457            _hasher: PhantomData,
458        }
459    }
460
461    /// Returns the selected leader index for the given round and seed signature.
462    ///
463    /// # Panics
464    ///
465    /// Panics if `n` is zero, or if a seed signature is missing after view 1.
466    #[allow(deprecated)]
467    pub fn select_leader<V: Variant>(
468        &self,
469        round: Round,
470        n: u32,
471        seed_signature: Option<V::Signature>,
472    ) -> Participant {
473        assert_ne!(n, 0, "no participants");
474        assert!(seed_signature.is_some() || round.view() == View::new(1));
475
476        let Some(seed_signature) = seed_signature else {
477            // Standard round-robin for view 1
478            let idx = round.epoch().get().wrapping_add(round.view().get()) % u64::from(n);
479            return Participant::new(u32::try_from(idx).expect("leader index fits in u32"));
480        };
481
482        // Use the seed signature as a source of randomness
483        let encoded = seed_signature.encode();
484        let index = match self.version {
485            RandomVersion::V0 => modulo(encoded.as_ref(), u64::from(n)),
486            RandomVersion::V1 => modulo(H::hash(&[encoded.as_ref()]).as_ref(), u64::from(n)),
487        };
488        Participant::new(u32::try_from(index).expect("leader index must fit in u32"))
489    }
490}
491
492impl<H: Hasher> Clone for Random<H> {
493    fn clone(&self) -> Self {
494        Self::new(self.version)
495    }
496}
497
498impl<H: Hasher> fmt::Debug for Random<H> {
499    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
500        self.version.fmt(f)
501    }
502}
503
504impl<P, V, H> Config<bls12381_threshold_vrf::Scheme<P, V>> for Random<H>
505where
506    P: PublicKey,
507    V: Variant,
508    H: Hasher,
509{
510    type Elector = RandomElector<bls12381_threshold_vrf::Scheme<P, V>, H>;
511
512    fn build(
513        self,
514        participants: &Set<P>,
515    ) -> RandomElector<bls12381_threshold_vrf::Scheme<P, V>, H> {
516        assert!(!participants.is_empty(), "no participants");
517        RandomElector {
518            n: participants.len() as u32,
519            version: self,
520            _phantom: PhantomData,
521        }
522    }
523}
524
525/// Initialized random leader elector using threshold signature randomness.
526///
527/// Created via [`Random::build`].
528pub struct RandomElector<S: Scheme, H: Hasher = Sha256> {
529    n: u32,
530    version: Random<H>,
531    _phantom: PhantomData<S>,
532}
533
534impl<S: Scheme, H: Hasher> Clone for RandomElector<S, H> {
535    fn clone(&self) -> Self {
536        Self {
537            n: self.n,
538            version: self.version.clone(),
539            _phantom: PhantomData,
540        }
541    }
542}
543
544impl<S: Scheme, H: Hasher> fmt::Debug for RandomElector<S, H> {
545    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
546        f.debug_struct("RandomElector")
547            .field("n", &self.n)
548            .field("version", &self.version)
549            .finish()
550    }
551}
552
553impl<P, V, H> Elector<bls12381_threshold_vrf::Scheme<P, V>>
554    for RandomElector<bls12381_threshold_vrf::Scheme<P, V>, H>
555where
556    P: PublicKey,
557    V: Variant,
558    H: Hasher,
559{
560    type Mode = Dynamic;
561
562    fn terms(&self) -> Terms {
563        Terms::rotating()
564    }
565
566    /// Selects the leader from the seed of `certificate`, or round-robin for
567    /// view 1, which has no certificate.
568    ///
569    /// # Panics
570    ///
571    /// Panics if `certificate` is `None` for a view other than 1, or if it was
572    /// not verified and its signature fails to decode.
573    fn elect(
574        &self,
575        round: Round,
576        certificate: Option<&bls12381_threshold_vrf::Certificate<V>>,
577    ) -> Participant {
578        self.version.select_leader::<V>(
579            round,
580            self.n,
581            certificate.map(|c| {
582                c.get()
583                    .expect("verified certificate must decode")
584                    .seed_signature
585            }),
586        )
587    }
588}
589
590#[cfg(test)]
591mod tests {
592    use super::*;
593    use crate::{
594        simplex::{
595            scheme::{bls12381_threshold::vrf as bls12381_threshold_vrf, ed25519},
596            types::Subject,
597        },
598        types::{Epoch, View},
599    };
600    use commonware_cryptography::{
601        Sha256, bls12381::primitives::variant::MinPk, certificate::mocks::Fixture,
602        sha256::Digest as Sha256Digest,
603    };
604    use commonware_parallel::Sequential;
605    use commonware_utils::{Faults, N3f1, NZU32, TryFromIterator, Widen, non_empty, test_rng};
606
607    const NAMESPACE: &[u8] = b"test";
608
609    type ThresholdScheme =
610        bls12381_threshold_vrf::Scheme<commonware_cryptography::ed25519::PublicKey, MinPk>;
611
612    #[test]
613    fn stable_terms_preserve_optimistic_views() {
614        let stall = Duration::from_secs(1);
615        let length = TermLength::new(NZU32!(5));
616
617        // The configured lookahead is stored verbatim, including values wider
618        // than the term (bounded by the issuance window, not by config) and
619        // zero (optimistic validation disabled).
620        for requested in [0, 3, 4, 5, 6, u64::MAX] {
621            let terms = Terms::stable(length, stall, ViewDelta::new(requested));
622            assert_eq!(terms.optimistic_views(), ViewDelta::new(requested));
623        }
624    }
625
626    #[test]
627    fn round_robin_rotates_through_participants() {
628        let mut rng = test_rng();
629        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
630        let participants = Set::try_from_iter(participants).unwrap();
631        let n = participants.len() as u32;
632        let elector: RoundRobinElector<ed25519::Scheme> =
633            RoundRobin::<Sha256>::default().build(&participants);
634        let epoch = Epoch::new(0);
635
636        // Run through 3 * n views, record the sequence of leaders
637        let mut leaders = Vec::new();
638        for view in 1..=(3 * u64::from(n)) {
639            let round = Round::new(epoch, View::new(view));
640            leaders.push(elector.elect(round, ()));
641        }
642
643        // Verify leaders cycle: consecutive leaders differ by 1 (mod n)
644        for i in 0..leaders.len() - 1 {
645            assert_eq!(Participant::new((leaders[i].get() + 1) % n), leaders[i + 1]);
646        }
647    }
648
649    #[test]
650    fn round_robin_cycles_through_epochs() {
651        let mut rng = test_rng();
652        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
653        let participants = Set::try_from_iter(participants).unwrap();
654        let n = participants.len();
655        let elector: RoundRobinElector<ed25519::Scheme> =
656            RoundRobin::<Sha256>::default().build(&participants);
657
658        // Record leader for view 1 of epochs 0..n
659        let leaders: Vec<_> = (0..Widen::widen(n))
660            .map(|e| {
661                let round = Round::new(Epoch::new(e), View::new(1));
662                elector.elect(round, ())
663            })
664            .collect();
665
666        // Each participant should be selected exactly once
667        let mut seen = vec![false; n];
668        for leader in &leaders {
669            assert!(!seen[usize::from(*leader)]);
670            seen[usize::from(*leader)] = true;
671        }
672        assert!(seen.iter().all(|x| *x));
673    }
674
675    #[test]
676    fn round_robin_handles_wrapping_epoch_plus_term_index() {
677        let mut rng = test_rng();
678        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
679        let participants = Set::try_from_iter(participants).unwrap();
680        let elector: RoundRobinElector<ed25519::Scheme> = RoundRobin::<Sha256>::default()
681            .with_term(
682                TermLength::new(NZU32!(5)),
683                Duration::from_secs(10),
684                ViewDelta::new(0),
685            )
686            .build(&participants);
687
688        let round = Round::new(Epoch::new(u64::MAX - 1), View::new(6));
689        let term_idx = round.view().term_index(TermLength::new(NZU32!(5)));
690        let expected = round.epoch().get().wrapping_add(term_idx) % 5;
691
692        assert_eq!(elector.elect(round, ()), Participant::new(expected as u32));
693    }
694
695    #[test]
696    fn round_robin_uses_stable_leaders_within_terms() {
697        let mut rng = test_rng();
698        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
699        let participants = Set::try_from_iter(participants).unwrap();
700        let elector: RoundRobinElector<ed25519::Scheme> = RoundRobin::<Sha256>::default()
701            .with_term(
702                TermLength::new(NZU32!(3)),
703                Duration::from_secs(10),
704                ViewDelta::new(0),
705            )
706            .build(&participants);
707        let epoch = Epoch::new(0);
708
709        let leader_v1 = elector.elect(Round::new(epoch, View::new(1)), ());
710        let leader_v2 = elector.elect(Round::new(epoch, View::new(2)), ());
711        let leader_v3 = elector.elect(Round::new(epoch, View::new(3)), ());
712        let leader_v4 = elector.elect(Round::new(epoch, View::new(4)), ());
713        let leader_v5 = elector.elect(Round::new(epoch, View::new(5)), ());
714        let leader_v6 = elector.elect(Round::new(epoch, View::new(6)), ());
715
716        assert_eq!(leader_v1, leader_v2);
717        assert_eq!(leader_v1, leader_v3);
718        assert_eq!(leader_v4, leader_v5);
719        assert_eq!(leader_v4, leader_v6);
720        assert_ne!(leader_v1, leader_v4);
721    }
722
723    #[test]
724    fn round_robin_epoch_transition_shifts_stable_term_leader() {
725        let mut rng = test_rng();
726        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
727        let participants = Set::try_from_iter(participants).unwrap();
728        let elector: RoundRobinElector<ed25519::Scheme> = RoundRobin::<Sha256>::default()
729            .with_term(
730                TermLength::new(NZU32!(3)),
731                Duration::from_secs(10),
732                ViewDelta::new(0),
733            )
734            .build(&participants);
735
736        let leader_epoch_0 = elector.elect(Round::new(Epoch::new(0), View::new(1)), ());
737        let leader_epoch_0_v2 = elector.elect(Round::new(Epoch::new(0), View::new(2)), ());
738        let leader_epoch_1 = elector.elect(Round::new(Epoch::new(1), View::new(1)), ());
739        let leader_epoch_1_v3 = elector.elect(Round::new(Epoch::new(1), View::new(3)), ());
740        let leader_epoch_2 = elector.elect(Round::new(Epoch::new(2), View::new(1)), ());
741        let leader_epoch_2_v2 = elector.elect(Round::new(Epoch::new(2), View::new(2)), ());
742
743        assert_eq!(leader_epoch_0, Participant::new(1));
744        assert_eq!(leader_epoch_0_v2, leader_epoch_0);
745        assert_eq!(leader_epoch_1, Participant::new(2));
746        assert_eq!(leader_epoch_1_v3, leader_epoch_1);
747        assert_eq!(leader_epoch_2, Participant::new(3));
748        assert_eq!(leader_epoch_2_v2, leader_epoch_2);
749    }
750
751    #[test]
752    fn round_robin_shuffled_changes_order() {
753        let mut rng = test_rng();
754        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
755        let participants = Set::try_from_iter(participants).unwrap();
756
757        let elector_no_seed: RoundRobinElector<ed25519::Scheme> =
758            RoundRobin::<Sha256>::default().build(&participants);
759        let elector_seed_1: RoundRobinElector<ed25519::Scheme> =
760            RoundRobin::<Sha256>::shuffled(b"seed1").build(&participants);
761        let elector_seed_2: RoundRobinElector<ed25519::Scheme> =
762            RoundRobin::<Sha256>::shuffled(b"seed2").build(&participants);
763
764        // Collect first 5 leaders from each
765        let epoch = Epoch::new(0);
766        let leaders_no_seed: Vec<_> = (1..=5)
767            .map(|v| elector_no_seed.elect(Round::new(epoch, View::new(v)), ()))
768            .collect();
769        let leaders_seed_1: Vec<_> = (1..=5)
770            .map(|v| elector_seed_1.elect(Round::new(epoch, View::new(v)), ()))
771            .collect();
772        let leaders_seed_2: Vec<_> = (1..=5)
773            .map(|v| elector_seed_2.elect(Round::new(epoch, View::new(v)), ()))
774            .collect();
775
776        // No seed should be identity permutation
777        assert_eq!(
778            leaders_no_seed,
779            vec![
780                Participant::new(1),
781                Participant::new(2),
782                Participant::new(3),
783                Participant::new(4),
784                Participant::new(0)
785            ]
786        );
787
788        // Different seeds should produce different permutations
789        assert_ne!(leaders_seed_1, leaders_no_seed);
790        assert_ne!(leaders_seed_2, leaders_no_seed);
791        assert_ne!(leaders_seed_1, leaders_seed_2);
792
793        // Each permutation should still cover all participants
794        for leaders in [&leaders_seed_1, &leaders_seed_2] {
795            let mut sorted = leaders.clone();
796            sorted.sort();
797            assert_eq!(
798                sorted,
799                vec![
800                    Participant::new(0),
801                    Participant::new(1),
802                    Participant::new(2),
803                    Participant::new(3),
804                    Participant::new(4)
805                ]
806            );
807        }
808    }
809
810    #[test]
811    fn round_robin_same_seed_is_deterministic() {
812        let mut rng = test_rng();
813        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
814        let participants = Set::try_from_iter(participants).unwrap();
815
816        let elector1: RoundRobinElector<ed25519::Scheme> =
817            RoundRobin::<Sha256>::shuffled(b"same_seed").build(&participants);
818        let elector2: RoundRobinElector<ed25519::Scheme> =
819            RoundRobin::<Sha256>::shuffled(b"same_seed").build(&participants);
820
821        let epoch = Epoch::new(0);
822        for view in 1..=10 {
823            let round = Round::new(epoch, View::new(view));
824            assert_eq!(elector1.elect(round, ()), elector2.elect(round, ()));
825        }
826    }
827
828    #[test]
829    #[should_panic(expected = "no participants")]
830    fn round_robin_build_panics_on_empty_participants() {
831        let participants: Set<commonware_cryptography::ed25519::PublicKey> = Set::default();
832        let _: RoundRobinElector<ed25519::Scheme> =
833            RoundRobin::<Sha256>::default().build(&participants);
834    }
835
836    #[test]
837    fn random_falls_back_to_round_robin_for_view_1() {
838        let mut rng = test_rng();
839        let Fixture { participants, .. } =
840            bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
841        let participants = Set::try_from_iter(participants).unwrap();
842        let n = participants.len();
843        let elector: RandomElector<ThresholdScheme> =
844            Random::new(RandomVersion::V1).build(&participants);
845
846        // For view 1 (no certificate), Random should behave like RoundRobin
847        let leaders: Vec<_> = (0..n as u64)
848            .map(|e| {
849                let round = Round::new(Epoch::new(e), View::new(1));
850                elector.elect(round, None)
851            })
852            .collect();
853
854        // Each participant should be selected exactly once (same as RoundRobin)
855        let mut seen = vec![false; n];
856        for leader in &leaders {
857            assert!(!seen[usize::from(*leader)]);
858            seen[usize::from(*leader)] = true;
859        }
860        assert!(seen.iter().all(|x| *x));
861    }
862
863    #[test]
864    fn random_fallback_does_not_truncate_before_modulo() {
865        // Five participants make truncation observable:
866        // 2^32 % 5 is 1, while (2^32 as u32) % 5 is 0
867        let mut rng = test_rng();
868        let Fixture { participants, .. } =
869            bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
870        let participants = Set::try_from_iter(participants).unwrap();
871        let random: RandomElector<ThresholdScheme> =
872            Random::new(RandomVersion::V1).build(&participants);
873        let round_robin: RoundRobinElector<ThresholdScheme> =
874            RoundRobin::<Sha256>::default().build(&participants);
875
876        // View 1 exercises Random's round-robin fallback
877        let round = Round::new(Epoch::new(u64::from(u32::MAX)), View::new(1));
878
879        // Both electors must preserve the full u64 sum through the modulo
880        assert_eq!(round_robin.elect(round, ()), Participant::new(1));
881        assert_eq!(random.elect(round, None), Participant::new(1));
882    }
883
884    #[test]
885    fn random_uses_certificate_randomness() {
886        let mut rng = test_rng();
887        let Fixture {
888            participants,
889            schemes,
890            ..
891        } = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
892        let participants = Set::try_from_iter(participants).unwrap();
893        let elector: RandomElector<ThresholdScheme> =
894            Random::new(RandomVersion::V1).build(&participants);
895        let quorum = N3f1::quorum(schemes.len()) as usize;
896
897        // Create certificate for round (1, 2)
898        let round1 = Round::new(Epoch::new(1), View::new(2));
899        let attestations1: Vec<_> = schemes
900            .iter()
901            .take(quorum)
902            .map(|s| {
903                s.sign::<Sha256Digest>(Subject::Nullify { round: round1 })
904                    .unwrap()
905            })
906            .collect();
907        let cert1 = schemes[0]
908            .assemble(non_empty![@attestations1], &Sequential)
909            .unwrap();
910
911        // Create certificate for round (1, 3) (different round -> different seed signature)
912        let round2 = Round::new(Epoch::new(1), View::new(3));
913        let attestations2: Vec<_> = schemes
914            .iter()
915            .take(quorum)
916            .map(|s| {
917                s.sign::<Sha256Digest>(Subject::Nullify { round: round2 })
918                    .unwrap()
919            })
920            .collect();
921        let cert2 = schemes[0]
922            .assemble(non_empty![@attestations2], &Sequential)
923            .unwrap();
924
925        // Same certificate always gives same leader
926        let leader1a = elector.elect(round1, Some(&cert1));
927        let leader1b = elector.elect(round1, Some(&cert1));
928        assert_eq!(leader1a, leader1b);
929
930        // Different certificates produce different leaders
931        //
932        // NOTE: In general, different certificates could produce the same leader by chance.
933        // However, for our specific test inputs (rng seed 42, 5 participants), we've
934        // verified these produce different results.
935        let leader2 = elector.elect(round1, Some(&cert2));
936        assert_ne!(leader1a, leader2);
937    }
938
939    #[test]
940    #[should_panic(expected = "no participants")]
941    fn random_build_panics_on_empty_participants() {
942        let participants: Set<commonware_cryptography::ed25519::PublicKey> = Set::default();
943        let _: RandomElector<ThresholdScheme> = Random::new(RandomVersion::V1).build(&participants);
944    }
945
946    #[test]
947    #[should_panic]
948    fn random_panics_on_none_certificate_after_view_1() {
949        let mut rng = test_rng();
950        let Fixture { participants, .. } =
951            bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
952        let participants = Set::try_from_iter(participants).unwrap();
953        let elector: RandomElector<ThresholdScheme> =
954            Random::new(RandomVersion::V1).build(&participants);
955
956        // View 2 requires a certificate
957        let round = Round::new(Epoch::new(1), View::new(2));
958        elector.elect(round, None);
959    }
960
961    mod conformance {
962        use super::*;
963        use commonware_codec::{Encode, Write};
964        use commonware_conformance::Conformance;
965        use commonware_cryptography::Sha256;
966        use rand::{RngExt as _, SeedableRng};
967        use rand_chacha::ChaCha8Rng;
968
969        /// Conformance test for shuffled RoundRobin leader election.
970        ///
971        /// Verifies that the permutation generated by `RoundRobin::shuffled`
972        /// remains deterministic across versions. This is critical because
973        /// changing the shuffle algorithm would cause consensus failures.
974        struct RoundRobinShuffleConformance;
975
976        impl Conformance for RoundRobinShuffleConformance {
977            async fn commit(seed: u64) -> Vec<u8> {
978                let mut rng = ChaCha8Rng::seed_from_u64(seed);
979
980                // Generate deterministic participants (using ed25519 fixture)
981                let n = rng.random_range(1..=100);
982                let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, n);
983                let participants = Set::try_from_iter(participants).unwrap();
984
985                // Generate a random seed for shuffling
986                let shuffle_seed: [u8; 32] = rng.random();
987
988                // Build the shuffled elector
989                let elector: RoundRobinElector<ed25519::Scheme> =
990                    RoundRobin::<Sha256>::shuffled(&shuffle_seed).build(&participants);
991
992                // Encode the permutation as the commitment
993                elector.permutation.encode().to_vec()
994            }
995        }
996
997        /// Conformance test for Random V0 leader election.
998        ///
999        /// Pins mapping the encoded threshold signature directly to a participant
1000        /// with modulo reduction.
1001        struct RandomV0SelectLeaderConformance;
1002
1003        /// Conformance test for Random V1 leader election.
1004        ///
1005        /// Pins hashing the encoded threshold signature before mapping it to a
1006        /// participant with modulo reduction.
1007        struct RandomV1SelectLeaderConformance;
1008
1009        fn random_select_leader_commit(seed: u64, version: Random) -> Vec<u8> {
1010            let mut rng = ChaCha8Rng::seed_from_u64(seed);
1011
1012            // Generate deterministic BLS threshold fixture (4-10 participants)
1013            let n = rng.random_range(4..=10);
1014            let Fixture {
1015                participants,
1016                schemes,
1017                ..
1018            } = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, n);
1019            let participants = Set::try_from_iter(participants).unwrap();
1020            let elector: RandomElector<ThresholdScheme> = version.build(&participants);
1021            let quorum =
1022                usize::try_from(N3f1::quorum(schemes.len())).expect("quorum exceeds usize::MAX");
1023
1024            // Generate deterministic round parameters
1025            let epoch = rng.random_range(0..1000);
1026            let view = rng.random_range(2..=101);
1027            let round = Round::new(Epoch::new(epoch), View::new(view));
1028
1029            // Create a valid threshold certificate
1030            let attestations: Vec<_> = schemes
1031                .iter()
1032                .take(quorum)
1033                .map(|s| s.sign::<Sha256Digest>(Subject::Nullify { round }).unwrap())
1034                .collect();
1035            let cert = schemes[0]
1036                .assemble(non_empty![@attestations], &Sequential)
1037                .unwrap();
1038
1039            // Elect leader using the certificate
1040            let leader = elector.elect(round, Some(&cert));
1041
1042            // Also test view 1 fallback (no certificate, round-robin)
1043            let round_v1 = Round::new(Epoch::new(epoch), View::new(1));
1044            let leader_v1 = elector.elect(round_v1, None);
1045
1046            // Commit both results
1047            let mut result = leader.encode_mut();
1048            leader_v1.write(&mut result);
1049            result.to_vec()
1050        }
1051
1052        #[allow(deprecated)]
1053        impl Conformance for RandomV0SelectLeaderConformance {
1054            async fn commit(seed: u64) -> Vec<u8> {
1055                random_select_leader_commit(seed, Random::new(RandomVersion::V0))
1056            }
1057        }
1058
1059        impl Conformance for RandomV1SelectLeaderConformance {
1060            async fn commit(seed: u64) -> Vec<u8> {
1061                random_select_leader_commit(seed, Random::new(RandomVersion::V1))
1062            }
1063        }
1064
1065        commonware_conformance::conformance_tests! {
1066            RoundRobinShuffleConformance => 512,
1067            RandomV0SelectLeaderConformance => 512,
1068            RandomV1SelectLeaderConformance => 512,
1069        }
1070    }
1071}