use crate::types::{ValidatorId, validator_stake::ValidatorStake};
use borsh::{BorshDeserialize, BorshSerialize};
use near_primitives_core::types::Balance;
use near_schema_checker_lib::ProtocolSchema;
mod compute_price;
#[derive(
BorshSerialize,
BorshDeserialize,
Default,
Copy,
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
ProtocolSchema,
)]
pub struct ValidatorMandatesConfig {
target_mandates_per_shard: usize,
num_shards: usize,
}
impl ValidatorMandatesConfig {
pub fn new(target_mandates_per_shard: usize, num_shards: usize) -> Self {
assert!(num_shards > 0, "there should be at least one shard");
Self { target_mandates_per_shard, num_shards }
}
}
#[derive(
BorshSerialize,
BorshDeserialize,
Default,
Clone,
Debug,
PartialEq,
Eq,
serde::Serialize,
ProtocolSchema,
)]
pub struct ValidatorMandates {
config: ValidatorMandatesConfig,
stake_per_mandate: Balance,
mandates: Vec<ValidatorId>,
partials: Vec<(ValidatorId, Balance)>,
}
impl ValidatorMandates {
pub fn new(config: ValidatorMandatesConfig, validators: &[ValidatorStake]) -> Self {
let stakes: Vec<Balance> = validators.iter().map(|v| v.stake()).collect();
let stake_per_mandate = compute_price::compute_mandate_price(config, &stakes);
let num_mandates_per_validator: Vec<u16> =
validators.iter().map(|v| v.num_mandates(stake_per_mandate)).collect();
let num_total_mandates =
num_mandates_per_validator.iter().map(|&num| usize::from(num)).sum();
let mut mandates: Vec<ValidatorId> = Vec::with_capacity(num_total_mandates);
for i in 0..validators.len() {
for _ in 0..num_mandates_per_validator[i] {
mandates.push(i as ValidatorId);
}
}
let mut partials = Vec::with_capacity(validators.len());
for i in 0..validators.len() {
let partial_weight = validators[i].partial_mandate_weight(stake_per_mandate);
if partial_weight > Balance::ZERO {
partials.push((i as ValidatorId, partial_weight));
}
}
Self { config, stake_per_mandate, mandates, partials }
}
}
#[cfg(feature = "rand")]
mod validator_mandates_sample {
use super::*;
use itertools::Itertools;
use rand::{Rng, seq::SliceRandom};
impl ValidatorMandates {
pub fn sample<R>(&self, rng: &mut R) -> ChunkValidatorStakeAssignment
where
R: Rng + ?Sized,
{
let shard_ids_for_mandates = ShuffledShardIds::new(rng, self.config.num_shards);
let shard_ids_for_partials = ShuffledShardIds::new(rng, self.config.num_shards);
let shuffled_mandates = self.shuffled_mandates(rng);
let shuffled_partials = self.shuffled_partials(rng);
let stake_per_mandate = self.stake_per_mandate;
let mut stake_assignment_per_shard =
vec![std::collections::HashMap::new(); self.config.num_shards];
for shard_id in 0..self.config.num_shards {
let mandates_assignment =
&mut stake_assignment_per_shard[shard_ids_for_mandates.get_alias(shard_id)];
for idx in (shard_id..shuffled_mandates.len()).step_by(self.config.num_shards) {
let validator_id = shuffled_mandates[idx];
let current_shard_validator_stake: &mut Balance =
mandates_assignment.entry(validator_id).or_default();
*current_shard_validator_stake =
current_shard_validator_stake.checked_add(stake_per_mandate).unwrap();
}
let partials_assignment =
&mut stake_assignment_per_shard[shard_ids_for_partials.get_alias(shard_id)];
for idx in (shard_id..shuffled_partials.len()).step_by(self.config.num_shards) {
let (validator_id, partial_weight) = shuffled_partials[idx];
let current_shard_validator_stake: &mut Balance =
partials_assignment.entry(validator_id).or_default();
*current_shard_validator_stake =
current_shard_validator_stake.checked_add(partial_weight).unwrap();
}
}
let mut ordered_stake_assignment_per_shard = Vec::with_capacity(self.config.num_shards);
for shard_id in 0..self.config.num_shards {
let stake_assignment = &stake_assignment_per_shard[shard_id];
let mut ordered_validator_ids = stake_assignment.keys().sorted().collect_vec();
ordered_validator_ids.shuffle(rng);
let ordered_mandate_assignment = ordered_validator_ids
.into_iter()
.map(|validator_id| (*validator_id, stake_assignment[validator_id]))
.collect_vec();
ordered_stake_assignment_per_shard.push(ordered_mandate_assignment);
}
ordered_stake_assignment_per_shard
}
pub(super) fn shuffled_mandates<R>(&self, rng: &mut R) -> Vec<ValidatorId>
where
R: Rng + ?Sized,
{
let mut shuffled_mandates = self.mandates.clone();
shuffled_mandates.shuffle(rng);
shuffled_mandates
}
pub(super) fn shuffled_partials<R>(&self, rng: &mut R) -> Vec<(ValidatorId, Balance)>
where
R: Rng + ?Sized,
{
let mut shuffled_partials = self.partials.clone();
shuffled_partials.shuffle(rng);
shuffled_partials
}
}
}
pub type ChunkValidatorStakeAssignment = Vec<Vec<(ValidatorId, Balance)>>;
#[cfg(feature = "rand")]
#[derive(Default, Clone, Debug, PartialEq, Eq)]
struct ShuffledShardIds {
shuffled_ids: Vec<usize>,
}
#[cfg(feature = "rand")]
impl ShuffledShardIds {
fn new<R>(rng: &mut R, num_shards: usize) -> Self
where
R: rand::Rng + ?Sized,
{
use rand::seq::SliceRandom;
let mut shuffled_ids = (0..num_shards).collect::<Vec<_>>();
shuffled_ids.shuffle(rng);
Self { shuffled_ids }
}
fn get_alias(&self, shard_id: usize) -> usize {
self.shuffled_ids[shard_id]
}
}
#[cfg(test)]
mod tests {
use super::{ChunkValidatorStakeAssignment, ShuffledShardIds, ValidatorMandates};
use crate::{
types::ValidatorId, types::validator_stake::ValidatorStake,
validator_mandates::ValidatorMandatesConfig,
};
use near_crypto::PublicKey;
use near_primitives_core::types::Balance;
use rand::SeedableRng;
use rand_chacha::ChaCha8Rng;
fn new_fixed_rng() -> ChaCha8Rng {
ChaCha8Rng::seed_from_u64(42)
}
#[test]
fn test_validator_mandates_config_new() {
let target_mandates_per_shard = 400;
let num_shards = 4;
assert_eq!(
ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards),
ValidatorMandatesConfig { target_mandates_per_shard, num_shards },
)
}
fn new_validator_stakes() -> Vec<ValidatorStake> {
let new_vs = |account_id: &str, balance: Balance| -> ValidatorStake {
ValidatorStake::new(
account_id.parse().unwrap(),
PublicKey::empty(near_crypto::KeyType::ED25519),
balance,
)
};
vec![
new_vs("account_0", Balance::from_yoctonear(30)),
new_vs("account_1", Balance::from_yoctonear(27)),
new_vs("account_2", Balance::from_yoctonear(9)),
new_vs("account_3", Balance::from_yoctonear(12)),
new_vs("account_4", Balance::from_yoctonear(35)),
new_vs("account_5", Balance::from_yoctonear(4)),
new_vs("account_6", Balance::from_yoctonear(6)),
]
}
#[test]
fn test_validator_mandates_new() {
let validators = new_validator_stakes();
let config = ValidatorMandatesConfig::new(3, 4);
let mandates = ValidatorMandates::new(config, &validators);
assert_eq!(mandates.stake_per_mandate, Balance::from_yoctonear(8));
let expected_mandates: Vec<ValidatorId> = vec![0, 0, 0, 1, 1, 1, 2, 3, 4, 4, 4, 4];
assert_eq!(mandates.mandates, expected_mandates);
assert_eq!(mandates.mandates.len(), config.num_shards * config.target_mandates_per_shard);
let expected_partials: Vec<(ValidatorId, Balance)> = vec![
(0, Balance::from_yoctonear(6)),
(1, Balance::from_yoctonear(3)),
(2, Balance::from_yoctonear(1)),
(3, Balance::from_yoctonear(4)),
(4, Balance::from_yoctonear(3)),
(5, Balance::from_yoctonear(4)),
(6, Balance::from_yoctonear(6)),
];
assert_eq!(mandates.partials, expected_partials);
}
#[test]
fn test_validator_mandates_shuffled_mandates() {
assert_validator_mandates_shuffled_mandates(3, vec![0, 1, 4, 4, 3, 1, 4, 0, 0]);
assert_validator_mandates_shuffled_mandates(4, vec![0, 0, 2, 1, 3, 4, 1, 1, 0, 4, 4, 4]);
}
fn assert_validator_mandates_shuffled_mandates(
num_shards: usize,
expected_assignment: Vec<ValidatorId>,
) {
let validators = new_validator_stakes();
let config = ValidatorMandatesConfig::new(3, num_shards);
let mandates = ValidatorMandates::new(config, &validators);
let mut rng = new_fixed_rng();
let _shard_ids_for_mandates = ShuffledShardIds::new(&mut rng, config.num_shards);
let _shard_ids_for_partials = ShuffledShardIds::new(&mut rng, config.num_shards);
let assignment = mandates.shuffled_mandates(&mut rng);
assert_eq!(
assignment, expected_assignment,
"Unexpected shuffling for num_shards = {num_shards}"
);
}
#[test]
fn test_validator_mandates_shuffled_partials() {
assert_validator_mandates_shuffled_partials(
3,
vec![
(3, Balance::from_yoctonear(2)),
(4, Balance::from_yoctonear(5)),
(1, Balance::from_yoctonear(7)),
(2, Balance::from_yoctonear(9)),
(5, Balance::from_yoctonear(4)),
(6, Balance::from_yoctonear(6)),
],
);
assert_validator_mandates_shuffled_partials(
4,
vec![
(5, Balance::from_yoctonear(4)),
(3, Balance::from_yoctonear(4)),
(0, Balance::from_yoctonear(6)),
(2, Balance::from_yoctonear(1)),
(1, Balance::from_yoctonear(3)),
(4, Balance::from_yoctonear(3)),
(6, Balance::from_yoctonear(6)),
],
);
}
fn assert_validator_mandates_shuffled_partials(
num_shards: usize,
expected_assignment: Vec<(ValidatorId, Balance)>,
) {
let validators = new_validator_stakes();
let config = ValidatorMandatesConfig::new(3, num_shards);
let mandates = ValidatorMandates::new(config, &validators);
let mut rng = new_fixed_rng();
let _shard_ids_for_mandates = ShuffledShardIds::new(&mut rng, config.num_shards);
let _shard_ids_for_partials = ShuffledShardIds::new(&mut rng, config.num_shards);
let _ = mandates.shuffled_mandates(&mut rng);
let assignment = mandates.shuffled_partials(&mut rng);
assert_eq!(
assignment, expected_assignment,
"Unexpected shuffling for num_shards = {num_shards}"
);
}
#[test]
fn test_validator_mandates_sample_even() {
let config = ValidatorMandatesConfig::new(3, 3);
let expected_assignment = vec![
vec![
(1, Balance::from_yoctonear(17)),
(4, Balance::from_yoctonear(10)),
(6, Balance::from_yoctonear(6)),
(0, Balance::from_yoctonear(10)),
],
vec![
(4, Balance::from_yoctonear(5)),
(5, Balance::from_yoctonear(4)),
(0, Balance::from_yoctonear(10)),
(1, Balance::from_yoctonear(10)),
(3, Balance::from_yoctonear(10)),
],
vec![
(0, Balance::from_yoctonear(10)),
(2, Balance::from_yoctonear(9)),
(4, Balance::from_yoctonear(20)),
(3, Balance::from_yoctonear(2)),
],
];
assert_validator_mandates_sample(config, expected_assignment);
}
#[test]
fn test_validator_mandates_sample_uneven() {
let config = ValidatorMandatesConfig::new(3, 4);
let expected_mandates_per_shards = vec![
vec![
(3, Balance::from_yoctonear(8)),
(6, Balance::from_yoctonear(6)),
(0, Balance::from_yoctonear(22)),
],
vec![
(4, Balance::from_yoctonear(8)),
(2, Balance::from_yoctonear(9)),
(1, Balance::from_yoctonear(8)),
],
vec![
(0, Balance::from_yoctonear(8)),
(3, Balance::from_yoctonear(4)),
(4, Balance::from_yoctonear(19)),
],
vec![
(4, Balance::from_yoctonear(8)),
(5, Balance::from_yoctonear(4)),
(1, Balance::from_yoctonear(19)),
],
];
assert_validator_mandates_sample(config, expected_mandates_per_shards);
}
fn assert_validator_mandates_sample(
config: ValidatorMandatesConfig,
expected_assignment: ChunkValidatorStakeAssignment,
) {
let validators = new_validator_stakes();
let mandates = ValidatorMandates::new(config, &validators);
let mut rng = new_fixed_rng();
let assignment = mandates.sample(&mut rng);
assert_eq!(assignment, expected_assignment);
}
#[test]
fn test_shuffled_shard_ids_new() {
let mut rng_3_shards = new_fixed_rng();
assert_shuffled_shard_ids(&mut rng_3_shards, 3, vec![2, 1, 0], "3 shards, 1st shuffle");
assert_shuffled_shard_ids(&mut rng_3_shards, 3, vec![2, 1, 0], "3 shards, 2nd shuffle");
let mut rng_4_shards = new_fixed_rng();
assert_shuffled_shard_ids(&mut rng_4_shards, 4, vec![0, 2, 1, 3], "4 shards, 1st shuffle");
assert_shuffled_shard_ids(&mut rng_4_shards, 4, vec![3, 2, 0, 1], "4 shards, 2nd shuffle");
}
fn assert_shuffled_shard_ids(
rng: &mut ChaCha8Rng,
num_shards: usize,
expected_shuffling: Vec<usize>,
test_descriptor: &str,
) {
let shuffled_ids_full_mandates = ShuffledShardIds::new(rng, num_shards);
assert_eq!(
shuffled_ids_full_mandates,
ShuffledShardIds { shuffled_ids: expected_shuffling },
"Unexpected shuffling for {test_descriptor}",
);
}
#[test]
fn test_shuffled_shard_ids_get_alias() {
let mut rng = new_fixed_rng();
let shuffled_ids = ShuffledShardIds::new(&mut rng, 4);
assert_eq!(shuffled_ids.get_alias(1), 2);
}
#[test]
fn test_deterministic_shuffle() {
let config = ValidatorMandatesConfig::new(3, 4);
let validators = new_validator_stakes();
let mandates = ValidatorMandates::new(config, &validators);
let mut rng1 = new_fixed_rng();
let assignment1 = mandates.sample(&mut rng1);
let mut rng2 = new_fixed_rng();
let assignment2 = mandates.sample(&mut rng2);
assert_eq!(assignment1, assignment2);
}
}