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