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