use crate::{
simplex::scheme::bls12381_threshold::vrf as bls12381_threshold_vrf,
types::{Participant, Round, TermLength, View, ViewDelta},
};
use commonware_codec::Encode;
use commonware_cryptography::{
Hasher, PublicKey, Sha256, bls12381::primitives::variant::Variant, certificate::Scheme,
};
use commonware_utils::{modulo, ordered::Set};
use std::{fmt, marker::PhantomData, time::Duration};
pub trait Config<S: Scheme>: Clone + Send + 'static {
type Elector: Elector<S>;
fn build(self, participants: &Set<S::PublicKey>) -> Self::Elector;
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Terms {
length: TermLength,
stall_timeout: Option<Duration>,
optimistic_views: ViewDelta,
}
impl Terms {
pub const fn rotating() -> Self {
Self {
length: TermLength::ONE,
stall_timeout: None,
optimistic_views: ViewDelta::zero(),
}
}
pub const fn stable(
length: TermLength,
stall_timeout: Duration,
optimistic_views: ViewDelta,
) -> Self {
assert!(
length.get() > 1,
"stable leaders require a term length greater than 1"
);
assert!(
!stall_timeout.is_zero(),
"stable leaders require a stall timeout greater than zero"
);
Self {
length,
stall_timeout: Some(stall_timeout),
optimistic_views,
}
}
pub const fn length(&self) -> TermLength {
self.length
}
pub const fn stall_timeout(&self) -> Option<Duration> {
self.stall_timeout
}
pub const fn optimistic_views(&self) -> ViewDelta {
self.optimistic_views
}
}
impl Default for Terms {
fn default() -> Self {
Self::rotating()
}
}
pub trait Elector<S: Scheme>: Clone + Send + 'static {
fn terms(&self) -> Terms;
fn elect(&self, round: Round, certificate: Option<&S::Certificate>) -> Participant;
}
#[derive(Debug, Default)]
pub struct RoundRobin<H: Hasher = Sha256> {
seed: Option<Vec<u8>>,
terms: Terms,
_phantom: PhantomData<H>,
}
impl<H: Hasher> Clone for RoundRobin<H> {
fn clone(&self) -> Self {
Self {
seed: self.seed.clone(),
terms: self.terms,
_phantom: PhantomData,
}
}
}
impl<H: Hasher> RoundRobin<H> {
pub fn shuffled(seed: &[u8]) -> Self {
Self {
seed: Some(seed.to_vec()),
terms: Terms::rotating(),
_phantom: PhantomData,
}
}
pub const fn with_term(
mut self,
term_length: TermLength,
stall_timeout: Duration,
optimistic_views: ViewDelta,
) -> Self {
self.terms = Terms::stable(term_length, stall_timeout, optimistic_views);
self
}
}
impl<S: Scheme, H: Hasher> Config<S> for RoundRobin<H> {
type Elector = RoundRobinElector<S>;
fn build(self, participants: &Set<S::PublicKey>) -> RoundRobinElector<S> {
assert!(!participants.is_empty(), "no participants");
let mut permutation: Vec<Participant> = (0..participants.len())
.map(Participant::from_usize)
.collect();
if let Some(seed) = &self.seed {
permutation.sort_by_key(|&index| H::hash(&[seed, &index.get().encode()]));
}
RoundRobinElector {
permutation,
terms: self.terms,
_phantom: PhantomData,
}
}
}
#[derive(Clone, Debug)]
pub struct RoundRobinElector<S: Scheme> {
permutation: Vec<Participant>,
terms: Terms,
_phantom: PhantomData<S>,
}
impl<S: Scheme> Elector<S> for RoundRobinElector<S> {
fn terms(&self) -> Terms {
self.terms
}
fn elect(&self, round: Round, _certificate: Option<&S::Certificate>) -> Participant {
let term_idx = round.view().term_index(self.terms.length());
let n = self.permutation.len();
let idx = round.epoch().get().wrapping_add(term_idx)
% u64::try_from(n).expect("permutation length fits in u64");
let idx = usize::try_from(idx).expect("leader index fits in usize");
self.permutation[idx]
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RandomVersion {
#[deprecated(
note = "mapping encoded threshold signature directly to participants can bias selection"
)]
V0,
V1,
}
pub struct Random<H: Hasher = Sha256> {
version: RandomVersion,
_hasher: PhantomData<H>,
}
impl<H: Hasher> Random<H> {
pub const fn new(version: RandomVersion) -> Self {
Self {
version,
_hasher: PhantomData,
}
}
#[allow(deprecated)]
pub fn select_leader<V: Variant>(
&self,
round: Round,
n: u32,
seed_signature: Option<V::Signature>,
) -> Participant {
assert_ne!(n, 0, "no participants");
assert!(seed_signature.is_some() || round.view() == View::new(1));
let Some(seed_signature) = seed_signature else {
let idx = round.epoch().get().wrapping_add(round.view().get()) % u64::from(n);
return Participant::new(u32::try_from(idx).expect("leader index fits in u32"));
};
let encoded = seed_signature.encode();
let index = match self.version {
RandomVersion::V0 => modulo(encoded.as_ref(), u64::from(n)),
RandomVersion::V1 => modulo(H::hash(&[encoded.as_ref()]).as_ref(), u64::from(n)),
};
Participant::new(u32::try_from(index).expect("leader index must fit in u32"))
}
}
impl<H: Hasher> Clone for Random<H> {
fn clone(&self) -> Self {
Self::new(self.version)
}
}
impl<H: Hasher> fmt::Debug for Random<H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.version.fmt(f)
}
}
impl<P, V, H> Config<bls12381_threshold_vrf::Scheme<P, V>> for Random<H>
where
P: PublicKey,
V: Variant,
H: Hasher,
{
type Elector = RandomElector<bls12381_threshold_vrf::Scheme<P, V>, H>;
fn build(
self,
participants: &Set<P>,
) -> RandomElector<bls12381_threshold_vrf::Scheme<P, V>, H> {
assert!(!participants.is_empty(), "no participants");
RandomElector {
n: participants.len() as u32,
version: self,
_phantom: PhantomData,
}
}
}
pub struct RandomElector<S: Scheme, H: Hasher = Sha256> {
n: u32,
version: Random<H>,
_phantom: PhantomData<S>,
}
impl<S: Scheme, H: Hasher> Clone for RandomElector<S, H> {
fn clone(&self) -> Self {
Self {
n: self.n,
version: self.version.clone(),
_phantom: PhantomData,
}
}
}
impl<S: Scheme, H: Hasher> fmt::Debug for RandomElector<S, H> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RandomElector")
.field("n", &self.n)
.field("version", &self.version)
.finish()
}
}
impl<P, V, H> Elector<bls12381_threshold_vrf::Scheme<P, V>>
for RandomElector<bls12381_threshold_vrf::Scheme<P, V>, H>
where
P: PublicKey,
V: Variant,
H: Hasher,
{
fn terms(&self) -> Terms {
Terms::rotating()
}
fn elect(
&self,
round: Round,
certificate: Option<&bls12381_threshold_vrf::Certificate<V>>,
) -> Participant {
self.version.select_leader::<V>(
round,
self.n,
certificate.map(|c| {
c.get()
.expect("verified certificate must decode")
.seed_signature
}),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
simplex::{
scheme::{bls12381_threshold::vrf as bls12381_threshold_vrf, ed25519},
types::Subject,
},
types::{Epoch, View},
};
use commonware_cryptography::{
Sha256, bls12381::primitives::variant::MinPk, certificate::mocks::Fixture,
sha256::Digest as Sha256Digest,
};
use commonware_parallel::Sequential;
use commonware_utils::{Faults, N3f1, NZU32, TryFromIterator, non_empty, test_rng};
const NAMESPACE: &[u8] = b"test";
type ThresholdScheme =
bls12381_threshold_vrf::Scheme<commonware_cryptography::ed25519::PublicKey, MinPk>;
#[test]
fn stable_terms_preserve_optimistic_views() {
let stall = Duration::from_secs(1);
let length = TermLength::new(NZU32!(5));
for requested in [0, 3, 4, 5, 6, u64::MAX] {
let terms = Terms::stable(length, stall, ViewDelta::new(requested));
assert_eq!(terms.optimistic_views(), ViewDelta::new(requested));
}
}
#[test]
fn round_robin_rotates_through_participants() {
let mut rng = test_rng();
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
let participants = Set::try_from_iter(participants).unwrap();
let n = participants.len() as u32;
let elector: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::default().build(&participants);
let epoch = Epoch::new(0);
let mut leaders = Vec::new();
for view in 1..=(3 * n as u64) {
let round = Round::new(epoch, View::new(view));
leaders.push(elector.elect(round, None));
}
for i in 0..leaders.len() - 1 {
assert_eq!(Participant::new((leaders[i].get() + 1) % n), leaders[i + 1]);
}
}
#[test]
fn round_robin_cycles_through_epochs() {
let mut rng = test_rng();
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let n = participants.len();
let elector: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::default().build(&participants);
let leaders: Vec<_> = (0..n as u64)
.map(|e| {
let round = Round::new(Epoch::new(e), View::new(1));
elector.elect(round, None)
})
.collect();
let mut seen = vec![false; n];
for leader in &leaders {
assert!(!seen[usize::from(*leader)]);
seen[usize::from(*leader)] = true;
}
assert!(seen.iter().all(|x| *x));
}
#[test]
fn round_robin_handles_wrapping_epoch_plus_term_index() {
let mut rng = test_rng();
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let elector: RoundRobinElector<ed25519::Scheme> = RoundRobin::<Sha256>::default()
.with_term(
TermLength::new(NZU32!(5)),
Duration::from_secs(10),
ViewDelta::new(0),
)
.build(&participants);
let round = Round::new(Epoch::new(u64::MAX - 1), View::new(6));
let term_idx = round.view().term_index(TermLength::new(NZU32!(5)));
let expected = round.epoch().get().wrapping_add(term_idx) % 5;
assert_eq!(
elector.elect(round, None),
Participant::new(expected as u32)
);
}
#[test]
fn round_robin_uses_stable_leaders_within_terms() {
let mut rng = test_rng();
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
let participants = Set::try_from_iter(participants).unwrap();
let elector: RoundRobinElector<ed25519::Scheme> = RoundRobin::<Sha256>::default()
.with_term(
TermLength::new(NZU32!(3)),
Duration::from_secs(10),
ViewDelta::new(0),
)
.build(&participants);
let epoch = Epoch::new(0);
let leader_v1 = elector.elect(Round::new(epoch, View::new(1)), None);
let leader_v2 = elector.elect(Round::new(epoch, View::new(2)), None);
let leader_v3 = elector.elect(Round::new(epoch, View::new(3)), None);
let leader_v4 = elector.elect(Round::new(epoch, View::new(4)), None);
let leader_v5 = elector.elect(Round::new(epoch, View::new(5)), None);
let leader_v6 = elector.elect(Round::new(epoch, View::new(6)), None);
assert_eq!(leader_v1, leader_v2);
assert_eq!(leader_v1, leader_v3);
assert_eq!(leader_v4, leader_v5);
assert_eq!(leader_v4, leader_v6);
assert_ne!(leader_v1, leader_v4);
}
#[test]
fn round_robin_epoch_transition_shifts_stable_term_leader() {
let mut rng = test_rng();
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 4);
let participants = Set::try_from_iter(participants).unwrap();
let elector: RoundRobinElector<ed25519::Scheme> = RoundRobin::<Sha256>::default()
.with_term(
TermLength::new(NZU32!(3)),
Duration::from_secs(10),
ViewDelta::new(0),
)
.build(&participants);
let leader_epoch_0 = elector.elect(Round::new(Epoch::new(0), View::new(1)), None);
let leader_epoch_0_v2 = elector.elect(Round::new(Epoch::new(0), View::new(2)), None);
let leader_epoch_1 = elector.elect(Round::new(Epoch::new(1), View::new(1)), None);
let leader_epoch_1_v3 = elector.elect(Round::new(Epoch::new(1), View::new(3)), None);
let leader_epoch_2 = elector.elect(Round::new(Epoch::new(2), View::new(1)), None);
let leader_epoch_2_v2 = elector.elect(Round::new(Epoch::new(2), View::new(2)), None);
assert_eq!(leader_epoch_0, Participant::new(1));
assert_eq!(leader_epoch_0_v2, leader_epoch_0);
assert_eq!(leader_epoch_1, Participant::new(2));
assert_eq!(leader_epoch_1_v3, leader_epoch_1);
assert_eq!(leader_epoch_2, Participant::new(3));
assert_eq!(leader_epoch_2_v2, leader_epoch_2);
}
#[test]
fn round_robin_shuffled_changes_order() {
let mut rng = test_rng();
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let elector_no_seed: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::default().build(&participants);
let elector_seed_1: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::shuffled(b"seed1").build(&participants);
let elector_seed_2: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::shuffled(b"seed2").build(&participants);
let epoch = Epoch::new(0);
let leaders_no_seed: Vec<_> = (1..=5)
.map(|v| elector_no_seed.elect(Round::new(epoch, View::new(v)), None))
.collect();
let leaders_seed_1: Vec<_> = (1..=5)
.map(|v| elector_seed_1.elect(Round::new(epoch, View::new(v)), None))
.collect();
let leaders_seed_2: Vec<_> = (1..=5)
.map(|v| elector_seed_2.elect(Round::new(epoch, View::new(v)), None))
.collect();
assert_eq!(
leaders_no_seed,
vec![
Participant::new(1),
Participant::new(2),
Participant::new(3),
Participant::new(4),
Participant::new(0)
]
);
assert_ne!(leaders_seed_1, leaders_no_seed);
assert_ne!(leaders_seed_2, leaders_no_seed);
assert_ne!(leaders_seed_1, leaders_seed_2);
for leaders in [&leaders_seed_1, &leaders_seed_2] {
let mut sorted = leaders.clone();
sorted.sort();
assert_eq!(
sorted,
vec![
Participant::new(0),
Participant::new(1),
Participant::new(2),
Participant::new(3),
Participant::new(4)
]
);
}
}
#[test]
fn round_robin_same_seed_is_deterministic() {
let mut rng = test_rng();
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let elector1: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::shuffled(b"same_seed").build(&participants);
let elector2: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::shuffled(b"same_seed").build(&participants);
let epoch = Epoch::new(0);
for view in 1..=10 {
let round = Round::new(epoch, View::new(view));
assert_eq!(elector1.elect(round, None), elector2.elect(round, None));
}
}
#[test]
#[should_panic(expected = "no participants")]
fn round_robin_build_panics_on_empty_participants() {
let participants: Set<commonware_cryptography::ed25519::PublicKey> = Set::default();
let _: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::default().build(&participants);
}
#[test]
fn random_falls_back_to_round_robin_for_view_1() {
let mut rng = test_rng();
let Fixture { participants, .. } =
bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let n = participants.len();
let elector: RandomElector<ThresholdScheme> =
Random::new(RandomVersion::V1).build(&participants);
let leaders: Vec<_> = (0..n as u64)
.map(|e| {
let round = Round::new(Epoch::new(e), View::new(1));
elector.elect(round, None)
})
.collect();
let mut seen = vec![false; n];
for leader in &leaders {
assert!(!seen[usize::from(*leader)]);
seen[usize::from(*leader)] = true;
}
assert!(seen.iter().all(|x| *x));
}
#[test]
fn random_fallback_does_not_truncate_before_modulo() {
let mut rng = test_rng();
let Fixture { participants, .. } =
bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let random: RandomElector<ThresholdScheme> =
Random::new(RandomVersion::V1).build(&participants);
let round_robin: RoundRobinElector<ThresholdScheme> =
RoundRobin::<Sha256>::default().build(&participants);
let round = Round::new(Epoch::new(u64::from(u32::MAX)), View::new(1));
assert_eq!(round_robin.elect(round, None), Participant::new(1));
assert_eq!(random.elect(round, None), Participant::new(1));
}
#[test]
fn random_uses_certificate_randomness() {
let mut rng = test_rng();
let Fixture {
participants,
schemes,
..
} = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let elector: RandomElector<ThresholdScheme> =
Random::new(RandomVersion::V1).build(&participants);
let quorum = N3f1::quorum(schemes.len()) as usize;
let round1 = Round::new(Epoch::new(1), View::new(2));
let attestations1: Vec<_> = schemes
.iter()
.take(quorum)
.map(|s| {
s.sign::<Sha256Digest>(Subject::Nullify { round: round1 })
.unwrap()
})
.collect();
let cert1 = schemes[0]
.assemble(non_empty![@attestations1], &Sequential)
.unwrap();
let round2 = Round::new(Epoch::new(1), View::new(3));
let attestations2: Vec<_> = schemes
.iter()
.take(quorum)
.map(|s| {
s.sign::<Sha256Digest>(Subject::Nullify { round: round2 })
.unwrap()
})
.collect();
let cert2 = schemes[0]
.assemble(non_empty![@attestations2], &Sequential)
.unwrap();
let leader1a = elector.elect(round1, Some(&cert1));
let leader1b = elector.elect(round1, Some(&cert1));
assert_eq!(leader1a, leader1b);
let leader2 = elector.elect(round1, Some(&cert2));
assert_ne!(leader1a, leader2);
}
#[test]
#[should_panic(expected = "no participants")]
fn random_build_panics_on_empty_participants() {
let participants: Set<commonware_cryptography::ed25519::PublicKey> = Set::default();
let _: RandomElector<ThresholdScheme> = Random::new(RandomVersion::V1).build(&participants);
}
#[test]
#[should_panic]
fn random_panics_on_none_certificate_after_view_1() {
let mut rng = test_rng();
let Fixture { participants, .. } =
bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, 5);
let participants = Set::try_from_iter(participants).unwrap();
let elector: RandomElector<ThresholdScheme> =
Random::new(RandomVersion::V1).build(&participants);
let round = Round::new(Epoch::new(1), View::new(2));
elector.elect(round, None);
}
mod conformance {
use super::*;
use commonware_codec::{Encode, Write};
use commonware_conformance::Conformance;
use commonware_cryptography::Sha256;
use rand::{RngExt as _, SeedableRng};
use rand_chacha::ChaCha8Rng;
struct RoundRobinShuffleConformance;
impl Conformance for RoundRobinShuffleConformance {
async fn commit(seed: u64) -> Vec<u8> {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let n = rng.random_range(1..=100);
let Fixture { participants, .. } = ed25519::fixture(&mut rng, NAMESPACE, n);
let participants = Set::try_from_iter(participants).unwrap();
let shuffle_seed: [u8; 32] = rng.random();
let elector: RoundRobinElector<ed25519::Scheme> =
RoundRobin::<Sha256>::shuffled(&shuffle_seed).build(&participants);
elector.permutation.encode().to_vec()
}
}
struct RandomV0SelectLeaderConformance;
struct RandomV1SelectLeaderConformance;
fn random_select_leader_commit(seed: u64, version: Random) -> Vec<u8> {
let mut rng = ChaCha8Rng::seed_from_u64(seed);
let n = rng.random_range(4..=10);
let Fixture {
participants,
schemes,
..
} = bls12381_threshold_vrf::fixture::<MinPk, _>(&mut rng, NAMESPACE, n);
let participants = Set::try_from_iter(participants).unwrap();
let elector: RandomElector<ThresholdScheme> = version.build(&participants);
let quorum =
usize::try_from(N3f1::quorum(schemes.len())).expect("quorum exceeds usize::MAX");
let epoch = rng.random_range(0..1000);
let view = rng.random_range(2..=101);
let round = Round::new(Epoch::new(epoch), View::new(view));
let attestations: Vec<_> = schemes
.iter()
.take(quorum)
.map(|s| s.sign::<Sha256Digest>(Subject::Nullify { round }).unwrap())
.collect();
let cert = schemes[0]
.assemble(non_empty![@attestations], &Sequential)
.unwrap();
let leader = elector.elect(round, Some(&cert));
let round_v1 = Round::new(Epoch::new(epoch), View::new(1));
let leader_v1 = elector.elect(round_v1, None);
let mut result = leader.encode_mut();
leader_v1.write(&mut result);
result.to_vec()
}
#[allow(deprecated)]
impl Conformance for RandomV0SelectLeaderConformance {
async fn commit(seed: u64) -> Vec<u8> {
random_select_leader_commit(seed, Random::new(RandomVersion::V0))
}
}
impl Conformance for RandomV1SelectLeaderConformance {
async fn commit(seed: u64) -> Vec<u8> {
random_select_leader_commit(seed, Random::new(RandomVersion::V1))
}
}
commonware_conformance::conformance_tests! {
RoundRobinShuffleConformance => 512,
RandomV0SelectLeaderConformance => 512,
RandomV1SelectLeaderConformance => 512,
}
}
}