#![allow(clippy::redundant_closure)]
use console::{
account::Address,
network::Network,
prelude::{cfg_into_iter, cfg_iter, cfg_reduce},
program::{Identifier, Literal, Plaintext, Value},
types::{Boolean, U64},
};
use ledger_committee::Committee;
use anyhow::{bail, ensure, Result};
use indexmap::{indexmap, IndexMap};
use std::str::FromStr;
#[cfg(not(feature = "serial"))]
use rayon::prelude::*;
pub fn committee_map_into_committee<N: Network>(
starting_round: u64,
committee_map: Vec<(Plaintext<N>, Value<N>)>,
) -> Result<Committee<N>> {
let microcredits_identifier = Identifier::from_str("microcredits")?;
let is_open_identifier = Identifier::from_str("is_open")?;
let committee_members = committee_map
.iter()
.map(|(key, value)| {
let address = match key {
Plaintext::Literal(Literal::Address(address), _) => address,
_ => bail!("Invalid committee key (missing address) - {key}"),
};
match value {
Value::Plaintext(Plaintext::Struct(state, _)) => {
let microcredits = match state.get(µcredits_identifier) {
Some(Plaintext::Literal(Literal::U64(microcredits), _)) => **microcredits,
_ => bail!("Invalid committee state (missing microcredits) - {value}"),
};
let is_open = match state.get(&is_open_identifier) {
Some(Plaintext::Literal(Literal::Boolean(is_open), _)) => **is_open,
_ => bail!("Invalid committee state (missing boolean) - {value}"),
};
Ok((*address, (microcredits, is_open)))
}
_ => bail!("Invalid committee value (missing struct) - {value}"),
}
})
.collect::<Result<IndexMap<_, _>>>()?;
Committee::new(starting_round, committee_members)
}
pub fn bonded_map_into_stakers<N: Network>(
bonded_map: Vec<(Plaintext<N>, Value<N>)>,
) -> Result<IndexMap<Address<N>, (Address<N>, u64)>> {
let validator_identifier = Identifier::from_str("validator")?;
let microcredits_identifier = Identifier::from_str("microcredits")?;
let convert = |key, value| {
let address = match key {
Plaintext::Literal(Literal::Address(address), _) => address,
_ => bail!("Invalid bonded key (missing staker) - {key}"),
};
match &value {
Value::Plaintext(Plaintext::Struct(state, _)) => {
let validator = match state.get(&validator_identifier) {
Some(Plaintext::Literal(Literal::Address(validator), _)) => *validator,
_ => bail!("Invalid bonded state (missing validator) - {value}"),
};
let microcredits = match state.get(µcredits_identifier) {
Some(Plaintext::Literal(Literal::U64(microcredits), _)) => **microcredits,
_ => bail!("Invalid bonded state (missing microcredits) - {value}"),
};
Ok((address, (validator, microcredits)))
}
_ => bail!("Invalid bonded value (missing struct) - {value}"),
}
};
bonded_map.into_iter().map(|(key, value)| convert(key, value)).collect::<Result<IndexMap<_, _>>>()
}
pub fn ensure_stakers_matches<N: Network>(
committee: &Committee<N>,
stakers: &IndexMap<Address<N>, (Address<N>, u64)>,
) -> Result<()> {
let validator_map: IndexMap<_, _> = cfg_reduce!(
cfg_into_iter!(stakers).map(|(_, (validator, microcredits))| indexmap! {*validator => *microcredits}),
|| IndexMap::new(),
|mut acc, e| {
for (validator, microcredits) in e {
let entry: &mut u64 = acc.entry(validator).or_default();
*entry = entry.saturating_add(microcredits);
}
acc
}
);
let total_microcredits =
cfg_reduce!(cfg_iter!(validator_map).map(|(_, microcredits)| *microcredits), || 0u64, |a, b| {
a.saturating_add(b)
});
ensure!(committee.members().len() == validator_map.len(), "Committee and validator map length do not match");
ensure!(committee.total_stake() == total_microcredits, "Committee and validator map total stake do not match");
for (validator, (microcredits, _)) in committee.members() {
let candidate_microcredits = validator_map.get(validator);
ensure!(candidate_microcredits.is_some(), "A validator is missing in finalize storage");
ensure!(
*microcredits == *candidate_microcredits.unwrap(),
"Committee contains an incorrect 'microcredits' amount from stakers"
);
}
Ok(())
}
pub fn to_next_committee<N: Network>(
current_committee: &Committee<N>,
next_round: u64,
next_stakers: &IndexMap<Address<N>, (Address<N>, u64)>,
) -> Result<Committee<N>> {
let validator_map: IndexMap<_, _> = cfg_reduce!(
cfg_into_iter!(next_stakers).map(|(_, (validator, microcredits))| indexmap! {*validator => *microcredits}),
|| IndexMap::new(),
|mut acc, e| {
for (validator, microcredits) in e {
let entry: &mut u64 = acc.entry(validator).or_default();
*entry = entry.saturating_add(microcredits);
}
acc
}
);
let mut members = IndexMap::with_capacity(validator_map.len());
for (validator, microcredits) in validator_map {
members.insert(validator, (microcredits, current_committee.is_committee_member_open(validator)));
}
Committee::new(next_round, members)
}
pub fn to_next_commitee_map_and_bonded_map<N: Network>(
next_committee: &Committee<N>,
next_stakers: &IndexMap<Address<N>, (Address<N>, u64)>,
) -> (Vec<(Plaintext<N>, Value<N>)>, Vec<(Plaintext<N>, Value<N>)>) {
let validator_identifier = Identifier::from_str("validator").expect("Failed to parse 'validator'");
let microcredits_identifier = Identifier::from_str("microcredits").expect("Failed to parse 'microcredits'");
let is_open_identifier = Identifier::from_str("is_open").expect("Failed to parse 'is_open'");
let committee_map = cfg_iter!(next_committee.members())
.map(|(validator, (microcredits, is_open))| {
let committee_state = indexmap! {
microcredits_identifier => Plaintext::from(Literal::U64(U64::new(*microcredits))),
is_open_identifier => Plaintext::from(Literal::Boolean(Boolean::new(*is_open))),
};
(
Plaintext::from(Literal::Address(*validator)),
Value::Plaintext(Plaintext::Struct(committee_state, Default::default())),
)
})
.collect::<Vec<_>>();
let bonded_map = cfg_iter!(next_stakers)
.map(|(staker, (validator, microcredits))| {
let bonded_state = indexmap! {
validator_identifier => Plaintext::from(Literal::Address(*validator)),
microcredits_identifier => Plaintext::from(Literal::U64(U64::new(*microcredits))),
};
(
Plaintext::from(Literal::Address(*staker)),
Value::Plaintext(Plaintext::Struct(bonded_state, Default::default())),
)
})
.collect::<Vec<_>>();
(committee_map, bonded_map)
}
#[cfg(test)]
pub(crate) mod test_helpers {
use super::*;
use crate::vm::TestRng;
use ledger_committee::MIN_VALIDATOR_STAKE;
use rand::{CryptoRng, Rng};
pub(crate) fn to_stakers<N: Network, R: Rng + CryptoRng>(
members: &IndexMap<Address<N>, (u64, bool)>,
rng: &mut R,
) -> IndexMap<Address<N>, (Address<N>, u64)> {
members
.into_iter()
.flat_map(|(validator, (microcredits, _))| {
let remaining_microcredits = microcredits.saturating_sub(MIN_VALIDATOR_STAKE);
let staker_amount = 10_000_000;
let num_iterations = (remaining_microcredits / staker_amount).saturating_sub(1);
let rngs = (0..num_iterations).map(|_| TestRng::from_seed(rng.gen())).collect::<Vec<_>>();
let mut stakers: IndexMap<_, _> = cfg_into_iter!(rngs)
.map(|mut rng| {
let staker = Address::<N>::new(rng.gen());
(staker, (*validator, staker_amount))
})
.collect();
stakers.insert(*validator, (*validator, MIN_VALIDATOR_STAKE));
let final_amount = remaining_microcredits.saturating_sub(num_iterations * staker_amount);
if final_amount > 0 {
let staker = Address::<N>::new(rng.gen());
stakers.insert(staker, (*validator, final_amount));
}
stakers
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
use console::prelude::TestRng;
#[allow(unused_imports)]
use rayon::prelude::*;
use std::str::FromStr;
fn to_committee_map<N: Network>(members: &IndexMap<Address<N>, (u64, bool)>) -> Vec<(Plaintext<N>, Value<N>)> {
members
.par_iter()
.map(|(validator, (microcredits, is_open))| {
let microcredits = U64::<N>::new(*microcredits);
let is_open = Boolean::<N>::new(*is_open);
(
Plaintext::from(Literal::Address(*validator)),
Value::from_str(&format!("{{ microcredits: {microcredits}, is_open: {is_open} }}")).unwrap(),
)
})
.collect()
}
fn to_bonded_map<N: Network>(stakers: &IndexMap<Address<N>, (Address<N>, u64)>) -> Vec<(Plaintext<N>, Value<N>)> {
let validator_identifier = Identifier::from_str("validator").expect("Failed to parse 'validator'");
let microcredits_identifier = Identifier::from_str("microcredits").expect("Failed to parse 'microcredits'");
stakers
.par_iter()
.map(|(staker, (validator, microcredits))| {
let bonded_state = indexmap! {
validator_identifier => Plaintext::from(Literal::Address(*validator)),
microcredits_identifier => Plaintext::from(Literal::U64(U64::new(*microcredits))),
};
(
Plaintext::from(Literal::Address(*staker)),
Value::Plaintext(Plaintext::Struct(bonded_state, Default::default())),
)
})
.collect()
}
#[test]
fn test_committee_map_into_committee() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 100, rng);
let committee_map = to_committee_map(committee.members());
let timer = std::time::Instant::now();
let candidate_committee = committee_map_into_committee(committee.starting_round(), committee_map).unwrap();
println!("committee_map_into_committee: {}ms", timer.elapsed().as_millis());
assert_eq!(candidate_committee, committee);
}
#[test]
fn test_bonded_map_into_stakers() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 100, rng);
let expected_stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let bonded_map = to_bonded_map(&expected_stakers);
let timer = std::time::Instant::now();
let candidate_stakers = bonded_map_into_stakers(bonded_map).unwrap();
println!("bonded_map_into_stakers: {}ms", timer.elapsed().as_millis());
assert_eq!(candidate_stakers.len(), expected_stakers.len());
assert_eq!(candidate_stakers, expected_stakers);
}
#[test]
fn test_ensure_stakers_matches() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 100, rng);
let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let timer = std::time::Instant::now();
let result = ensure_stakers_matches(&committee, &stakers);
println!("ensure_stakers_matches: {}ms", timer.elapsed().as_millis());
assert!(result.is_ok());
}
#[test]
fn test_to_next_committee() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee_for_round_and_size(1, 100, rng);
let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let timer = std::time::Instant::now();
let next_committee = to_next_committee(&committee, committee.starting_round() + 1, &stakers).unwrap();
println!("to_next_committee: {}ms", timer.elapsed().as_millis());
assert_eq!(committee.starting_round() + 1, next_committee.starting_round());
assert_eq!(committee.members(), next_committee.members());
}
#[test]
fn test_to_next_commitee_map_and_bonded_map() {
let rng = &mut TestRng::default();
let committee = ledger_committee::test_helpers::sample_committee(rng);
let stakers = crate::committee::test_helpers::to_stakers(committee.members(), rng);
let timer = std::time::Instant::now();
let (committee_map, bonded_map) = to_next_commitee_map_and_bonded_map(&committee, &stakers);
println!("to_next_commitee_map_and_bonded_map: {}ms", timer.elapsed().as_millis());
assert_eq!(committee_map, to_committee_map(committee.members()));
assert_eq!(bonded_map, to_bonded_map(&stakers));
}
}