use {
super::ValidatorMandatesConfig,
near_primitives_core::types::Balance,
std::cmp::{Ordering, min},
};
pub fn compute_mandate_price(config: ValidatorMandatesConfig, stakes: &[Balance]) -> Balance {
let ValidatorMandatesConfig { target_mandates_per_shard, num_shards } = config;
let total_stake =
stakes.iter().copied().fold(Balance::ZERO, |sum, item| sum.saturating_add(item));
let target_mandates: u128 = min(
num_shards.saturating_mul(target_mandates_per_shard) as u128,
total_stake.as_yoctonear(),
);
binary_search(Balance::from_yoctonear(1), total_stake, target_mandates, |mandate_price| {
stakes.iter().fold(0, |sum, item| {
sum.saturating_add(item.as_yoctonear() / mandate_price.as_yoctonear())
})
})
}
fn binary_search<F>(low: Balance, high: Balance, target: u128, f: F) -> Balance
where
F: Fn(Balance) -> u128,
{
debug_assert!(low < high);
let mut low = low;
let mut high = high;
if f(low) == target {
return highest_exact(low, high, target, f);
} else if f(high) == target {
return high;
}
while high.checked_sub(low).unwrap() > Balance::from_yoctonear(1) {
let mid = low.checked_add(high.checked_sub(low).unwrap().checked_div(2).unwrap()).unwrap();
let f_mid = f(mid);
match f_mid.cmp(&target) {
Ordering::Equal => return highest_exact(mid, high, target, f),
Ordering::Less => high = mid,
Ordering::Greater => low = mid,
}
}
low
}
fn highest_exact<F>(low: Balance, high: Balance, target: u128, f: F) -> Balance
where
F: Fn(Balance) -> u128,
{
debug_assert!(low < high);
debug_assert_eq!(f(low), target);
debug_assert!(f(high) < target);
let mut low = low;
let mut high = high;
while high.checked_sub(low).unwrap() > Balance::from_yoctonear(1) {
let mid = low.checked_add(high.checked_sub(low).unwrap().checked_div(2).unwrap()).unwrap();
let f_mid = f(mid);
match f_mid.cmp(&target) {
Ordering::Equal => low = mid,
Ordering::Less => high = mid,
Ordering::Greater => unreachable!("Given function must be non-increasing"),
}
}
low
}
#[cfg(test)]
mod tests {
use super::*;
use rand::{Rng, SeedableRng};
#[test]
fn test_small_total_stake() {
let stakes = [Balance::from_yoctonear(100); 1];
let num_shards = 1;
let target_mandates_per_shard = 1000;
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
assert_eq!(compute_mandate_price(config, &stakes), Balance::from_yoctonear(1));
}
#[test]
fn test_constant_dist() {
let stakes = [Balance::from_yoctonear(11); 13];
let num_shards = 1;
let target_mandates_per_shard = stakes.len();
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
assert_eq!(compute_mandate_price(config, &stakes), stakes[0]);
let target_mandates_per_shard = 2 * stakes.len();
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
assert_eq!(compute_mandate_price(config, &stakes), stakes[0].checked_div(2).unwrap());
let target_mandates_per_shard = stakes.len() - 1;
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
assert_eq!(compute_mandate_price(config, &stakes), stakes[0]);
}
#[test]
fn test_step_dist() {
let stakes = {
let mut buf = [Balance::from_yoctonear(11); 13];
let n = buf.len() / 2;
for s in buf.iter_mut().take(n) {
*s = s.checked_mul(5).unwrap();
}
buf
};
let num_shards = 1;
let target_mandates_per_shard = stakes.len();
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard + 5);
let target_mandates_per_shard = 2 * stakes.len();
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard + 11);
let target_mandates_per_shard = stakes.len() / 2;
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard);
}
#[test]
fn test_exp_dist() {
let stakes = {
let mut buf = vec![Balance::from_yoctonear(1_000_000_000); 210];
let mut last_stake = buf[0];
for s in buf.iter_mut().skip(1) {
last_stake = last_stake.checked_mul(97).unwrap().checked_div(100).unwrap();
*s = last_stake;
}
buf
};
let num_shards = 6;
let target_mandates_per_shard = 68;
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard * num_shards);
let num_shards = 1;
let target_mandates_per_shard = stakes.len();
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard);
let target_mandates_per_shard = stakes.len() * 2;
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard);
let target_mandates_per_shard = stakes.len() / 2;
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard);
}
#[test]
fn test_rand_dist() {
let stakes = {
let mut stakes = vec![Balance::ZERO; 1000];
let mut rng = rand::rngs::StdRng::seed_from_u64(0xdeadbeef);
for s in &mut stakes {
*s = Balance::from_yoctonear(rng.gen_range(1..10_000));
}
stakes
};
let num_shards = 1;
let target_mandates_per_shard = stakes.len();
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard + 3);
let target_mandates_per_shard = 2 * stakes.len();
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard);
let target_mandates_per_shard = stakes.len() / 2;
let config = ValidatorMandatesConfig::new(target_mandates_per_shard, num_shards);
let price = compute_mandate_price(config, &stakes);
assert_eq!(count_whole_mandates(&stakes, price), target_mandates_per_shard);
}
fn count_whole_mandates(stakes: &[Balance], mandate_price: Balance) -> usize {
stakes.iter().fold(0u128, |sum, item| {
sum.saturating_add(item.as_yoctonear() / mandate_price.as_yoctonear())
}) as usize
}
}