use crate::party::Party;
use crate::value::{Value, ValueSelector};
use rand_chacha::rand_core::{RngCore, SeedableRng};
use rand_chacha::ChaCha20Rng;
use std::hash::{DefaultHasher, Hash, Hasher};
use thiserror::Error;
pub trait LeaderElector<V: Value, VS: ValueSelector<V>>: Send {
fn elect_leader(&self, party: &Party<V, VS>) -> Result<u64, Box<dyn std::error::Error>>;
}
#[derive(Clone, Debug, Default)]
pub struct DefaultLeaderElector {}
impl DefaultLeaderElector {
pub fn new() -> Self {
Self::default()
}
fn compute_seed<V: Value, VS: ValueSelector<V>>(party: &Party<V, VS>) -> u64 {
let mut hasher = DefaultHasher::new();
party.cfg.party_weights.hash(&mut hasher);
party.cfg.threshold.hash(&mut hasher);
party.ballot.hash(&mut hasher);
hasher.finish()
}
fn hash_to_range(seed: u64, range: u128) -> u128 {
let mut k = 128;
while 1u128 << (k - 1) > range {
k -= 1;
}
let mut rng = ChaCha20Rng::seed_from_u64(seed);
loop {
let mut raw_res = ((rng.next_u64() as u128) << 64) | (rng.next_u64() as u128);
raw_res >>= 128 - k;
if raw_res < range {
return raw_res;
}
}
}
}
#[derive(Error, Debug)]
pub enum DefaultLeaderElectorError {
#[error("zero weight sum")]
ZeroWeightSum,
}
impl<V: Value, VS: ValueSelector<V>> LeaderElector<V, VS> for DefaultLeaderElector {
fn elect_leader(&self, party: &Party<V, VS>) -> Result<u64, Box<dyn std::error::Error>> {
let seed = DefaultLeaderElector::compute_seed(party);
let total_weight: u128 = party.cfg.party_weights.iter().map(|&x| x as u128).sum();
if total_weight == 0 {
return Err(DefaultLeaderElectorError::ZeroWeightSum.into());
}
let random_value = DefaultLeaderElector::hash_to_range(seed, total_weight);
let mut cumulative_sum = 0u128;
for (index, &weight) in party.cfg.party_weights.iter().enumerate() {
cumulative_sum += weight as u128;
if random_value <= cumulative_sum {
return Ok(index as u64);
}
}
unreachable!("Index is guaranteed to be returned in a loop.")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::BPConConfig;
use crate::test_mocks::MockParty;
use rand::Rng;
use std::thread;
use std::time::Duration;
#[test]
fn test_default_leader_elector_weight_one() {
let mut party = MockParty::default();
party.cfg.party_weights = vec![0, 1, 0, 0];
let elector = DefaultLeaderElector::new();
let leader = elector.elect_leader(&party).unwrap();
println!("leader: {}", leader);
}
#[test]
fn test_default_leader_elector_determinism() {
let party = MockParty::default();
let elector = DefaultLeaderElector::new();
const ITERATIONS: usize = 10;
let leaders: Vec<_> = (0..ITERATIONS)
.map(|_| elector.elect_leader(&party).unwrap())
.collect();
match &leaders[..] {
[first_leader, rest @ ..] => {
assert!(
rest.iter().all(|leader| leader == first_leader),
"All leaders should be the same across multiple iterations."
);
}
_ => panic!("No leaders were collected!"),
}
}
#[test]
fn test_default_leader_elector_fail_with_zero_weights() {
let mut party = MockParty::default();
let cfg = BPConConfig {
party_weights: vec![0, 0, 0],
..Default::default()
};
party.cfg = cfg;
let elector = DefaultLeaderElector::new();
assert!(
elector.elect_leader(&party).is_err(),
"Expected DefaultLeaderElectorError::ZeroWeightSum"
);
}
fn debug_hash_to_range_new(seed: u64, range: u64) -> u64 {
assert!(range > 1);
let mut k = 64;
while 1u64 << (k - 1) >= range {
k -= 1;
}
let mut rng = ChaCha20Rng::seed_from_u64(seed);
let mut iteration = 1u64;
loop {
let mut raw_res: u64 = rng.next_u64();
raw_res >>= 64 - k;
if raw_res < range {
return raw_res;
}
iteration += 1;
assert!(iteration <= 50)
}
}
#[test]
#[ignore = "takes too long to run, launch manually"]
fn test_hash_range_random() {
const N: usize = 37;
const M: i64 = 10000000;
let mut cnt1: [i64; N] = [0; N];
for _ in 0..M {
let mut rng = rand::thread_rng();
let seed: u64 = rng.random();
let res1 = debug_hash_to_range_new(seed, N as u64);
assert!(res1 < N as u64);
cnt1[res1 as usize] += 1;
}
println!("1: {:?}", cnt1);
let mut avg1: i64 = 0;
for item in cnt1.iter().take(N) {
avg1 += (M / (N as i64) - item).abs();
}
avg1 /= N as i64;
println!("Avg 1: {}", avg1);
}
#[test]
fn test_rng() {
let mut rng1 = ChaCha20Rng::seed_from_u64(123456);
let mut rng2 = ChaCha20Rng::seed_from_u64(123456);
println!("{}", rng1.next_u64());
println!("{}", rng2.next_u64());
thread::sleep(Duration::from_secs(2));
println!("{}", rng1.next_u64());
println!("{}", rng2.next_u64());
}
}