use std::{
collections::{BTreeMap, BTreeSet, HashMap},
fmt::{Display, Formatter, Write},
hash::{Hash, Hasher},
};
use fastcrypto::traits::KeyPair;
pub use iota_protocol_config::ProtocolVersion;
use once_cell::sync::OnceCell;
use rand::{
Rng, SeedableRng,
rngs::{StdRng, ThreadRng},
seq::SliceRandom,
};
use serde::{Deserialize, Serialize};
use super::base_types::*;
use crate::{
crypto::{AuthorityKeyPair, AuthorityPublicKey, random_committee_key_pairs_of_size},
error::{IotaError, IotaResult},
multiaddr::Multiaddr,
};
pub type EpochId = u64;
pub type StakeUnit = u64;
pub type CommitteeDigest = [u8; 32];
pub const TOTAL_VOTING_POWER: StakeUnit = 10_000;
pub const QUORUM_THRESHOLD: StakeUnit = 6_667;
pub const VALIDITY_THRESHOLD: StakeUnit = 3_334;
#[derive(Clone, Debug, Serialize, Deserialize, Eq)]
pub struct Committee {
pub epoch: EpochId,
pub voting_rights: Vec<(AuthorityName, StakeUnit)>,
expanded_keys: HashMap<AuthorityName, AuthorityPublicKey>,
index_map: HashMap<AuthorityName, usize>,
}
impl Committee {
pub fn new(epoch: EpochId, voting_rights: BTreeMap<AuthorityName, StakeUnit>) -> Self {
let mut voting_rights: Vec<(AuthorityName, StakeUnit)> =
voting_rights.iter().map(|(a, s)| (*a, *s)).collect();
assert!(!voting_rights.is_empty());
assert!(voting_rights.iter().any(|(_, s)| *s != 0));
voting_rights.sort_by_key(|(a, _)| *a);
let total_votes: StakeUnit = voting_rights.iter().map(|(_, votes)| *votes).sum();
assert_eq!(total_votes, TOTAL_VOTING_POWER);
let (expanded_keys, index_map) = Self::load_inner(&voting_rights);
Committee {
epoch,
voting_rights,
expanded_keys,
index_map,
}
}
pub fn new_for_testing_with_normalized_voting_power(
epoch: EpochId,
mut voting_weights: BTreeMap<AuthorityName, StakeUnit>,
) -> Self {
let num_nodes = voting_weights.len();
let total_votes: StakeUnit = voting_weights.values().cloned().sum();
let normalization_coef = TOTAL_VOTING_POWER as f64 / total_votes as f64;
let mut total_sum = 0;
for (idx, (_auth, weight)) in voting_weights.iter_mut().enumerate() {
if idx < num_nodes - 1 {
*weight = (*weight as f64 * normalization_coef).floor() as u64; total_sum += *weight;
} else {
*weight = TOTAL_VOTING_POWER - total_sum;
}
}
Self::new(epoch, voting_weights)
}
pub fn load_inner(
voting_rights: &[(AuthorityName, StakeUnit)],
) -> (
HashMap<AuthorityName, AuthorityPublicKey>,
HashMap<AuthorityName, usize>,
) {
let expanded_keys: HashMap<AuthorityName, AuthorityPublicKey> = voting_rights
.iter()
.map(|(addr, _)| {
(
*addr,
(*addr)
.try_into()
.expect("Validator pubkey is always verified on-chain"),
)
})
.collect();
let index_map: HashMap<AuthorityName, usize> = voting_rights
.iter()
.enumerate()
.map(|(index, (addr, _))| (*addr, index))
.collect();
(expanded_keys, index_map)
}
pub fn authority_index(&self, author: &AuthorityName) -> Option<u32> {
self.index_map.get(author).map(|i| *i as u32)
}
pub fn authority_by_index(&self, index: u32) -> Option<&AuthorityName> {
self.voting_rights.get(index as usize).map(|(name, _)| name)
}
pub fn epoch(&self) -> EpochId {
self.epoch
}
pub fn public_key(&self, authority: &AuthorityName) -> IotaResult<&AuthorityPublicKey> {
debug_assert_eq!(self.expanded_keys.len(), self.voting_rights.len());
match self.expanded_keys.get(authority) {
Some(v) => Ok(v),
None => Err(IotaError::InvalidCommittee(format!(
"Authority #{} not found, committee size {}",
authority,
self.expanded_keys.len()
))),
}
}
pub fn sample(&self) -> &AuthorityName {
Self::choose_multiple_weighted(&self.voting_rights[..], 1, &mut ThreadRng::default())
.next()
.unwrap()
}
fn choose_multiple_weighted<'a>(
slice: &'a [(AuthorityName, StakeUnit)],
count: usize,
rng: &mut impl Rng,
) -> impl Iterator<Item = &'a AuthorityName> {
slice
.choose_multiple_weighted(rng, count, |(_, weight)| *weight as f64)
.unwrap()
.map(|(a, _)| a)
}
pub fn choose_multiple_weighted_iter(
&self,
count: usize,
) -> impl Iterator<Item = &AuthorityName> {
self.voting_rights
.choose_multiple_weighted(&mut ThreadRng::default(), count, |(_, weight)| {
*weight as f64
})
.unwrap()
.map(|(a, _)| a)
}
pub fn total_votes(&self) -> StakeUnit {
TOTAL_VOTING_POWER
}
pub fn quorum_threshold(&self) -> StakeUnit {
QUORUM_THRESHOLD
}
pub fn validity_threshold(&self) -> StakeUnit {
VALIDITY_THRESHOLD
}
pub fn threshold<const STRENGTH: bool>(&self) -> StakeUnit {
if STRENGTH {
QUORUM_THRESHOLD
} else {
VALIDITY_THRESHOLD
}
}
pub fn num_members(&self) -> usize {
self.voting_rights.len()
}
pub fn members(&self) -> impl Iterator<Item = &(AuthorityName, StakeUnit)> {
self.voting_rights.iter()
}
pub fn names(&self) -> impl Iterator<Item = &AuthorityName> {
self.voting_rights.iter().map(|(name, _)| name)
}
pub fn stakes(&self) -> impl Iterator<Item = StakeUnit> + '_ {
self.voting_rights.iter().map(|(_, stake)| *stake)
}
pub fn authority_exists(&self, name: &AuthorityName) -> bool {
self.voting_rights
.binary_search_by_key(name, |(a, _)| *a)
.is_ok()
}
pub fn shuffle_by_stake_from_tx_digest(
&self,
tx_digest: &TransactionDigest,
) -> Vec<AuthorityName> {
let digest_bytes = tx_digest.into_inner();
let mut rng = StdRng::from_seed(digest_bytes);
self.shuffle_by_stake_with_rng(None, None, &mut rng)
}
pub fn new_simple_test_committee_of_size(size: usize) -> (Self, Vec<AuthorityKeyPair>) {
let key_pairs: Vec<_> = random_committee_key_pairs_of_size(size)
.into_iter()
.collect();
let committee = Self::new_for_testing_with_normalized_voting_power(
0,
key_pairs
.iter()
.map(|key| {
(AuthorityName::from(key.public()), 1)
})
.collect(),
);
(committee, key_pairs)
}
pub fn new_simple_test_committee() -> (Self, Vec<AuthorityKeyPair>) {
Self::new_simple_test_committee_of_size(4)
}
}
impl CommitteeTrait<AuthorityName> for Committee {
fn shuffle_by_stake_with_rng(
&self,
preferences: Option<&BTreeSet<AuthorityName>>,
restrict_to: Option<&BTreeSet<AuthorityName>>,
rng: &mut impl Rng,
) -> Vec<AuthorityName> {
let restricted = self
.voting_rights
.iter()
.filter(|(name, _)| {
if let Some(restrict_to) = restrict_to {
restrict_to.contains(name)
} else {
true
}
})
.cloned();
let (preferred, rest): (Vec<_>, Vec<_>) = if let Some(preferences) = preferences {
restricted.partition(|(name, _)| preferences.contains(name))
} else {
(Vec::new(), restricted.collect())
};
Self::choose_multiple_weighted(&preferred, preferred.len(), rng)
.chain(Self::choose_multiple_weighted(&rest, rest.len(), rng))
.cloned()
.collect()
}
fn weight(&self, author: &AuthorityName) -> StakeUnit {
match self.voting_rights.binary_search_by_key(author, |(a, _)| *a) {
Err(_) => 0,
Ok(idx) => self.voting_rights[idx].1,
}
}
}
impl PartialEq for Committee {
fn eq(&self, other: &Self) -> bool {
self.epoch == other.epoch && self.voting_rights == other.voting_rights
}
}
impl Hash for Committee {
fn hash<H: Hasher>(&self, state: &mut H) {
self.epoch.hash(state);
self.voting_rights.hash(state);
}
}
impl Display for Committee {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
let mut voting_rights = String::new();
for (name, vote) in &self.voting_rights {
write!(voting_rights, "{}: {}, ", name.concise(), vote)?;
}
write!(
f,
"Committee (epoch={:?}, voting_rights=[{}])",
self.epoch, voting_rights
)
}
}
pub trait CommitteeTrait<K: Ord> {
fn shuffle_by_stake_with_rng(
&self,
preferences: Option<&BTreeSet<K>>,
restrict_to: Option<&BTreeSet<K>>,
rng: &mut impl Rng,
) -> Vec<K>;
fn shuffle_by_stake(
&self,
preferences: Option<&BTreeSet<K>>,
restrict_to: Option<&BTreeSet<K>>,
) -> Vec<K> {
self.shuffle_by_stake_with_rng(preferences, restrict_to, &mut ThreadRng::default())
}
fn weight(&self, author: &K) -> StakeUnit;
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct NetworkMetadata {
pub network_address: Multiaddr,
pub primary_address: Multiaddr,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct CommitteeWithNetworkMetadata {
epoch_id: EpochId,
validators: BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)>,
#[serde(skip)]
committee: OnceCell<Committee>,
}
impl CommitteeWithNetworkMetadata {
pub fn new(
epoch_id: EpochId,
validators: BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)>,
) -> Self {
Self {
epoch_id,
validators,
committee: OnceCell::new(),
}
}
pub fn epoch(&self) -> EpochId {
self.epoch_id
}
pub fn validators(&self) -> &BTreeMap<AuthorityName, (StakeUnit, NetworkMetadata)> {
&self.validators
}
pub fn committee(&self) -> &Committee {
self.committee.get_or_init(|| {
Committee::new(
self.epoch_id,
self.validators
.iter()
.map(|(name, (stake, _))| (*name, *stake))
.collect(),
)
})
}
}
impl Display for CommitteeWithNetworkMetadata {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(
f,
"CommitteeWithNetworkMetadata (epoch={}, validators={:?})",
self.epoch_id, self.validators
)
}
}
#[cfg(test)]
mod test {
use fastcrypto::traits::KeyPair;
use super::*;
use crate::crypto::{AuthorityKeyPair, get_key_pair};
#[test]
fn test_shuffle_by_weight() {
let (_, sec1): (_, AuthorityKeyPair) = get_key_pair();
let (_, sec2): (_, AuthorityKeyPair) = get_key_pair();
let (_, sec3): (_, AuthorityKeyPair) = get_key_pair();
let a1: AuthorityName = sec1.public().into();
let a2: AuthorityName = sec2.public().into();
let a3: AuthorityName = sec3.public().into();
let mut authorities = BTreeMap::new();
authorities.insert(a1, 1);
authorities.insert(a2, 1);
authorities.insert(a3, 1);
let committee = Committee::new_for_testing_with_normalized_voting_power(0, authorities);
assert_eq!(committee.shuffle_by_stake(None, None).len(), 3);
let mut pref = BTreeSet::new();
pref.insert(a2);
for _ in 0..100 {
assert_eq!(
a2,
*committee
.shuffle_by_stake(Some(&pref), None)
.first()
.unwrap()
);
}
let mut restrict = BTreeSet::new();
restrict.insert(a2);
for _ in 0..100 {
let res = committee.shuffle_by_stake(None, Some(&restrict));
assert_eq!(1, res.len());
assert_eq!(a2, res[0]);
}
let res = committee.shuffle_by_stake(Some(&BTreeSet::new()), None);
assert_eq!(3, res.len());
let res = committee.shuffle_by_stake(None, Some(&BTreeSet::new()));
assert_eq!(0, res.len());
}
}