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//! This module uses a type-state pattern to ensure correct usage:
24//! 1. Users create an elector [`Config`] (e.g., [`RoundRobin`])
25//! 2. The config is passed to the consensus configuration
26//! 3. Consensus calls [`Config::build`] internally with the correct participants
27//! 4. The resulting [`Elector`] can only be created by consensus, preventing misuse
28
29use crate::{
30    simplex::scheme::bls12381_threshold::vrf as bls12381_threshold_vrf,
31    types::{Participant, Round, View},
32};
33use commonware_codec::Encode;
34use commonware_cryptography::{
35    bls12381::primitives::variant::Variant, certificate::Scheme, Hasher, PublicKey, Sha256,
36};
37use commonware_utils::{modulo, ordered::Set};
38use std::marker::PhantomData;
39
40/// Configuration for creating an [`Elector`].
41///
42/// Users create and configure this type, then pass it to the consensus configuration.
43/// Consensus will call [`build`](Config::build) internally with the correct
44/// participant set to create the initialized [`Elector`].
45///
46/// # Determinism Requirement
47///
48/// Implementations **must** be deterministic: given the same construction parameters
49/// and the same inputs to [`Elector::elect`], the method must always return
50/// the same leader index. This is critical for consensus correctness - all honest
51/// participants must agree on the leader for each round.
52pub trait Config<S: Scheme>: Clone + Default + Send + 'static {
53    /// The initialized elector type.
54    type Elector: Elector<S>;
55
56    /// Builds the elector with the given participants.
57    ///
58    /// Called internally by consensus with the correct participant set.
59    ///
60    /// # Panics
61    ///
62    /// Implementations should panic if `participants` is empty.
63    fn build(self, participants: &Set<S::PublicKey>) -> Self::Elector;
64}
65
66/// An initialized elector that can select leaders for consensus rounds.
67///
68/// This type can only be created via [`Config::build`], which is called
69/// internally by consensus. This ensures the elector is always initialized with
70/// the correct participant set.
71///
72/// # Certificate Handling
73///
74/// The `certificate` parameter to [`elect`](Elector::elect) is `None` only for
75/// view 1 (the first view after genesis). For all subsequent views, a certificate
76/// from the previous view is provided. Implementations can use the certificate to
77/// derive randomness (like [`RandomElector`]) or ignore it entirely (like [`RoundRobinElector`]).
78pub trait Elector<S: Scheme>: Clone + Send + 'static {
79    /// Selects the leader for the given round.
80    ///
81    /// This method **must** be a pure function given the elector's initialization state.
82    ///
83    /// The `certificate` is expected to be `None` only for view 1.
84    ///
85    /// Returns the index of the selected leader in the participants list.
86    fn elect(&self, round: Round, certificate: Option<&S::Certificate>) -> Participant;
87}
88
89/// Configuration for round-robin leader election.
90///
91/// Rotates through participants based on `(epoch + view) % num_participants`.
92/// The rotation order can be shuffled at construction using a seed.
93///
94/// Works with any signing scheme.
95#[derive(Clone, Debug, Default)]
96pub struct RoundRobin<H: Hasher = Sha256> {
97    seed: Option<Vec<u8>>,
98    _phantom: PhantomData<H>,
99}
100
101impl<H: Hasher> RoundRobin<H> {
102    /// Creates a round-robin config that will shuffle the rotation order based on seed.
103    ///
104    /// The seed is used during [`Config::build`] to deterministically
105    /// shuffle the permutation.
106    pub fn shuffled(seed: &[u8]) -> Self {
107        Self {
108            seed: Some(seed.to_vec()),
109            _phantom: PhantomData,
110        }
111    }
112}
113
114impl<S: Scheme, H: Hasher> Config<S> for RoundRobin<H> {
115    type Elector = RoundRobinElector<S>;
116
117    fn build(self, participants: &Set<S::PublicKey>) -> RoundRobinElector<S> {
118        assert!(!participants.is_empty(), "no participants");
119
120        let mut permutation: Vec<Participant> = (0..participants.len())
121            .map(Participant::from_usize)
122            .collect();
123
124        if let Some(seed) = &self.seed {
125            let mut hasher = H::new();
126            permutation.sort_by_key(|&index| {
127                hasher.update(seed);
128                hasher.update(&index.get().encode());
129                hasher.finalize()
130            });
131        }
132
133        RoundRobinElector {
134            permutation,
135            _phantom: PhantomData,
136        }
137    }
138}
139
140/// Initialized round-robin leader elector.
141///
142/// Created via [`RoundRobin::build`].
143#[derive(Clone, Debug)]
144pub struct RoundRobinElector<S: Scheme> {
145    permutation: Vec<Participant>,
146    _phantom: PhantomData<S>,
147}
148
149impl<S: Scheme> Elector<S> for RoundRobinElector<S> {
150    fn elect(&self, round: Round, _certificate: Option<&S::Certificate>) -> Participant {
151        let n = self.permutation.len();
152        let idx = (round.epoch().get().wrapping_add(round.view().get())) as usize % n;
153        self.permutation[idx]
154    }
155}
156
157/// Configuration for leader election using threshold signature randomness.
158///
159/// Uses the seed signature from BLS threshold certificates to derive unpredictable
160/// leader selection. Falls back to standard round-robin for view 1 when no
161/// certificate is available.
162///
163/// Only works with [`super::scheme::bls12381_threshold::vrf`]
164/// (implements [`super::scheme::bls12381_threshold::vrf::Seedable`]).
165#[derive(Clone, Debug, Default)]
166pub struct Random;
167
168impl Random {
169    /// Returns the selected leader index for the given round and seed signature.
170    pub fn select_leader<V: Variant>(
171        round: Round,
172        n: u32,
173        seed_signature: Option<V::Signature>,
174    ) -> Participant {
175        assert!(seed_signature.is_some() || round.view() == View::new(1));
176
177        let Some(seed_signature) = seed_signature else {
178            // Standard round-robin for view 1
179            return Participant::new(
180                (round.epoch().get().wrapping_add(round.view().get())) as u32 % n,
181            );
182        };
183
184        // Use the seed signature as a source of randomness
185        Participant::new(modulo(seed_signature.encode().as_ref(), n as u64) as u32)
186    }
187}
188
189impl<P, V> Config<bls12381_threshold_vrf::Scheme<P, V>> for Random
190where
191    P: PublicKey,
192    V: Variant,
193{
194    type Elector = RandomElector<bls12381_threshold_vrf::Scheme<P, V>>;
195
196    fn build(self, participants: &Set<P>) -> RandomElector<bls12381_threshold_vrf::Scheme<P, V>> {
197        assert!(!participants.is_empty(), "no participants");
198        RandomElector {
199            n: participants.len() as u32,
200            _phantom: PhantomData,
201        }
202    }
203}
204
205/// Initialized random leader elector using threshold signature randomness.
206///
207/// Created via [`Random::build`].
208#[derive(Clone, Debug)]
209pub struct RandomElector<S: Scheme> {
210    n: u32,
211    _phantom: PhantomData<S>,
212}
213
214impl<P, V> Elector<bls12381_threshold_vrf::Scheme<P, V>>
215    for RandomElector<bls12381_threshold_vrf::Scheme<P, V>>
216where
217    P: PublicKey,
218    V: Variant,
219{
220    fn elect(
221        &self,
222        round: Round,
223        certificate: Option<&bls12381_threshold_vrf::Certificate<V>>,
224    ) -> Participant {
225        Random::select_leader::<V>(
226            round,
227            self.n,
228            certificate.map(|c| {
229                c.get()
230                    .expect("verified certificate must decode")
231                    .seed_signature
232            }),
233        )
234    }
235}
236
237#[cfg(test)]
238mod tests {
239    use super::*;
240    use crate::{
241        simplex::{
242            scheme::{bls12381_threshold::vrf as bls12381_threshold_vrf, ed25519},
243            types::Subject,
244        },
245        types::{Epoch, View},
246    };
247    use commonware_cryptography::{
248        bls12381::primitives::variant::MinPk, certificate::mocks::Fixture,
249        sha256::Digest as Sha256Digest, Sha256,
250    };
251    use commonware_parallel::Sequential;
252    use commonware_utils::{test_rng, Faults, N3f1, TryFromIterator};
253
254    const NAMESPACE: &[u8] = b"test";
255
256    type ThresholdScheme =
257        bls12381_threshold_vrf::Scheme<commonware_cryptography::ed25519::PublicKey, MinPk>;
258
259    #[test]
260    fn round_robin_rotates_through_participants() {
261        let mut rng = test_rng();
262        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
263        let participants = Set::try_from_iter(participants).unwrap();
264        let n = participants.len() as u32;
265        let elector: RoundRobinElector<ed25519::Scheme> =
266            RoundRobin::<Sha256>::default().build(&participants);
267        let epoch = Epoch::new(0);
268
269        // Run through 3 * n views, record the sequence of leaders
270        let mut leaders = Vec::new();
271        for view in 1..=(3 * n as u64) {
272            let round = Round::new(epoch, View::new(view));
273            leaders.push(elector.elect(round, None));
274        }
275
276        // Verify leaders cycle: consecutive leaders differ by 1 (mod n)
277        for i in 0..leaders.len() - 1 {
278            assert_eq!(Participant::new((leaders[i].get() + 1) % n), leaders[i + 1]);
279        }
280    }
281
282    #[test]
283    fn round_robin_cycles_through_epochs() {
284        let mut rng = test_rng();
285        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
286        let participants = Set::try_from_iter(participants).unwrap();
287        let n = participants.len();
288        let elector: RoundRobinElector<ed25519::Scheme> =
289            RoundRobin::<Sha256>::default().build(&participants);
290
291        // Record leader for view 1 of epochs 0..n
292        let leaders: Vec<_> = (0..n as u64)
293            .map(|e| {
294                let round = Round::new(Epoch::new(e), View::new(1));
295                elector.elect(round, None)
296            })
297            .collect();
298
299        // Each participant should be selected exactly once
300        let mut seen = vec![false; n];
301        for leader in &leaders {
302            assert!(!seen[usize::from(*leader)]);
303            seen[usize::from(*leader)] = true;
304        }
305        assert!(seen.iter().all(|x| *x));
306    }
307
308    #[test]
309    fn round_robin_shuffled_changes_order() {
310        let mut rng = test_rng();
311        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
312        let participants = Set::try_from_iter(participants).unwrap();
313
314        let elector_no_seed: RoundRobinElector<ed25519::Scheme> =
315            RoundRobin::<Sha256>::default().build(&participants);
316        let elector_seed_1: RoundRobinElector<ed25519::Scheme> =
317            RoundRobin::<Sha256>::shuffled(b"seed1").build(&participants);
318        let elector_seed_2: RoundRobinElector<ed25519::Scheme> =
319            RoundRobin::<Sha256>::shuffled(b"seed2").build(&participants);
320
321        // Collect first 5 leaders from each
322        let epoch = Epoch::new(0);
323        let leaders_no_seed: Vec<_> = (1..=5)
324            .map(|v| elector_no_seed.elect(Round::new(epoch, View::new(v)), None))
325            .collect();
326        let leaders_seed_1: Vec<_> = (1..=5)
327            .map(|v| elector_seed_1.elect(Round::new(epoch, View::new(v)), None))
328            .collect();
329        let leaders_seed_2: Vec<_> = (1..=5)
330            .map(|v| elector_seed_2.elect(Round::new(epoch, View::new(v)), None))
331            .collect();
332
333        // No seed should be identity permutation
334        assert_eq!(
335            leaders_no_seed,
336            vec![
337                Participant::new(1),
338                Participant::new(2),
339                Participant::new(3),
340                Participant::new(4),
341                Participant::new(0)
342            ]
343        );
344
345        // Different seeds should produce different permutations
346        assert_ne!(leaders_seed_1, leaders_no_seed);
347        assert_ne!(leaders_seed_2, leaders_no_seed);
348        assert_ne!(leaders_seed_1, leaders_seed_2);
349
350        // Each permutation should still cover all participants
351        for leaders in [&leaders_seed_1, &leaders_seed_2] {
352            let mut sorted = leaders.clone();
353            sorted.sort();
354            assert_eq!(
355                sorted,
356                vec![
357                    Participant::new(0),
358                    Participant::new(1),
359                    Participant::new(2),
360                    Participant::new(3),
361                    Participant::new(4)
362                ]
363            );
364        }
365    }
366
367    #[test]
368    fn round_robin_same_seed_is_deterministic() {
369        let mut rng = test_rng();
370        let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
371        let participants = Set::try_from_iter(participants).unwrap();
372
373        let elector1: RoundRobinElector<ed25519::Scheme> =
374            RoundRobin::<Sha256>::shuffled(b"same_seed").build(&participants);
375        let elector2: RoundRobinElector<ed25519::Scheme> =
376            RoundRobin::<Sha256>::shuffled(b"same_seed").build(&participants);
377
378        let epoch = Epoch::new(0);
379        for view in 1..=10 {
380            let round = Round::new(epoch, View::new(view));
381            assert_eq!(elector1.elect(round, None), elector2.elect(round, None));
382        }
383    }
384
385    #[test]
386    #[should_panic(expected = "no participants")]
387    fn round_robin_build_panics_on_empty_participants() {
388        let participants: Set<commonware_cryptography::ed25519::PublicKey> = Set::default();
389        let _: RoundRobinElector<ed25519::Scheme> =
390            RoundRobin::<Sha256>::default().build(&participants);
391    }
392
393    #[test]
394    fn random_falls_back_to_round_robin_for_view_1() {
395        let mut rng = test_rng();
396        let Fixture { participants, .. } =
397            bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
398        let participants = Set::try_from_iter(participants).unwrap();
399        let n = participants.len();
400        let elector: RandomElector<ThresholdScheme> = Random.build(&participants);
401
402        // For view 1 (no certificate), Random should behave like RoundRobin
403        let leaders: Vec<_> = (0..n as u64)
404            .map(|e| {
405                let round = Round::new(Epoch::new(e), View::new(1));
406                elector.elect(round, None)
407            })
408            .collect();
409
410        // Each participant should be selected exactly once (same as RoundRobin)
411        let mut seen = vec![false; n];
412        for leader in &leaders {
413            assert!(!seen[usize::from(*leader)]);
414            seen[usize::from(*leader)] = true;
415        }
416        assert!(seen.iter().all(|x| *x));
417    }
418
419    #[test]
420    fn random_uses_certificate_randomness() {
421        let mut rng = test_rng();
422        let Fixture {
423            participants,
424            schemes,
425            ..
426        } = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
427        let participants = Set::try_from_iter(participants).unwrap();
428        let elector: RandomElector<ThresholdScheme> = Random.build(&participants);
429        let quorum = N3f1::quorum(schemes.len()) as usize;
430
431        // Create certificate for round (1, 2)
432        let round1 = Round::new(Epoch::new(1), View::new(2));
433        let attestations1: Vec<_> = schemes
434            .iter()
435            .take(quorum)
436            .map(|s| {
437                s.sign::<Sha256Digest>(Subject::Nullify { round: round1 })
438                    .unwrap()
439            })
440            .collect();
441        let cert1 = schemes[0]
442            .assemble::<_, N3f1>(attestations1, &Sequential)
443            .unwrap();
444
445        // Create certificate for round (1, 3) (different round -> different seed signature)
446        let round2 = Round::new(Epoch::new(1), View::new(3));
447        let attestations2: Vec<_> = schemes
448            .iter()
449            .take(quorum)
450            .map(|s| {
451                s.sign::<Sha256Digest>(Subject::Nullify { round: round2 })
452                    .unwrap()
453            })
454            .collect();
455        let cert2 = schemes[0]
456            .assemble::<_, N3f1>(attestations2, &Sequential)
457            .unwrap();
458
459        // Same certificate always gives same leader
460        let leader1a = elector.elect(round1, Some(&cert1));
461        let leader1b = elector.elect(round1, Some(&cert1));
462        assert_eq!(leader1a, leader1b);
463
464        // Different certificates produce different leaders
465        //
466        // NOTE: In general, different certificates could produce the same leader by chance.
467        // However, for our specific test inputs (rng seed 42, 5 participants), we've
468        // verified these produce different results.
469        let leader2 = elector.elect(round1, Some(&cert2));
470        assert_ne!(leader1a, leader2);
471    }
472
473    #[test]
474    #[should_panic(expected = "no participants")]
475    fn random_build_panics_on_empty_participants() {
476        let participants: Set<commonware_cryptography::ed25519::PublicKey> = Set::default();
477        let _: RandomElector<ThresholdScheme> = Random.build(&participants);
478    }
479
480    #[test]
481    #[should_panic]
482    fn random_panics_on_none_certificate_after_view_1() {
483        let mut rng = test_rng();
484        let Fixture { participants, .. } =
485            bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
486        let participants = Set::try_from_iter(participants).unwrap();
487        let elector: RandomElector<ThresholdScheme> = Random.build(&participants);
488
489        // View 2 requires a certificate
490        let round = Round::new(Epoch::new(1), View::new(2));
491        elector.elect(round, None);
492    }
493
494    mod conformance {
495        use super::*;
496        use commonware_codec::{Encode, Write};
497        use commonware_conformance::Conformance;
498        use commonware_cryptography::Sha256;
499        use rand::{Rng, SeedableRng};
500        use rand_chacha::ChaCha8Rng;
501
502        /// Conformance test for shuffled RoundRobin leader election.
503        ///
504        /// Verifies that the permutation generated by `RoundRobin::shuffled`
505        /// remains deterministic across versions. This is critical because
506        /// changing the shuffle algorithm would cause consensus failures.
507        struct RoundRobinShuffleConformance;
508
509        impl Conformance for RoundRobinShuffleConformance {
510            async fn commit(seed: u64) -> Vec<u8> {
511                let mut rng = ChaCha8Rng::seed_from_u64(seed);
512
513                // Generate deterministic participants (using ed25519 fixture)
514                let n = rng.gen_range(1..=100);
515                let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, n);
516                let participants = Set::try_from_iter(participants).unwrap();
517
518                // Generate a random seed for shuffling
519                let shuffle_seed: [u8; 32] = rng.gen();
520
521                // Build the shuffled elector
522                let elector: RoundRobinElector<ed25519::Scheme> =
523                    RoundRobin::<Sha256>::shuffled(&shuffle_seed).build(&participants);
524
525                // Encode the permutation as the commitment
526                elector.permutation.encode().to_vec()
527            }
528        }
529
530        /// Conformance test for Random leader election.
531        ///
532        /// Verifies that `Random::select_leader` produces deterministic results
533        /// given the same inputs. This tests the `modulo` function usage and
534        /// threshold signature encoding for leader selection.
535        struct RandomSelectLeaderConformance;
536
537        impl Conformance for RandomSelectLeaderConformance {
538            async fn commit(seed: u64) -> Vec<u8> {
539                let mut rng = ChaCha8Rng::seed_from_u64(seed);
540
541                // Generate deterministic BLS threshold fixture (4-10 participants)
542                let n = rng.gen_range(4..=10);
543                let Fixture {
544                    participants,
545                    schemes,
546                    ..
547                } = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, n);
548                let participants = Set::try_from_iter(participants).unwrap();
549                let elector: RandomElector<ThresholdScheme> = Random.build(&participants);
550                let quorum = N3f1::quorum(schemes.len()) as usize;
551
552                // Generate deterministic round parameters
553                let epoch = rng.gen_range(0..1000);
554                let view = rng.gen_range(2..=101);
555
556                let round = Round::new(Epoch::new(epoch), View::new(view));
557
558                // Create a valid threshold certificate
559                let attestations: Vec<_> = schemes
560                    .iter()
561                    .take(quorum)
562                    .map(|s| s.sign::<Sha256Digest>(Subject::Nullify { round }).unwrap())
563                    .collect();
564                let cert = schemes[0]
565                    .assemble::<_, N3f1>(attestations, &Sequential)
566                    .unwrap();
567
568                // Elect leader using the certificate
569                let leader = elector.elect(round, Some(&cert));
570
571                // Also test view 1 fallback (no certificate, round-robin)
572                let round_v1 = Round::new(Epoch::new(epoch), View::new(1));
573                let leader_v1 = elector.elect(round_v1, None);
574
575                // Commit both results
576                let mut result = leader.encode_mut();
577                leader_v1.write(&mut result);
578                result.to_vec()
579            }
580        }
581
582        commonware_conformance::conformance_tests! {
583            RoundRobinShuffleConformance => 512,
584            RandomSelectLeaderConformance => 512,
585        }
586    }
587}