Feature/multi node simulator (#1692)
* simple multi-node simulator * Extending simulator with multi-node feature + testing against known good values * Mixnet contract test fixes * comment explaining the epsilon choice
This commit is contained in:
committed by
GitHub
parent
5d583548ec
commit
33a339ae2c
@@ -3,8 +3,8 @@
|
||||
|
||||
use cosmwasm_std::Decimal;
|
||||
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal) {
|
||||
let epsilon = Decimal::from_ratio(1u128, 100_000_000u128);
|
||||
pub fn compare_decimals(a: Decimal, b: Decimal, epsilon: Option<Decimal>) {
|
||||
let epsilon = epsilon.unwrap_or_else(|| Decimal::from_ratio(1u128, 100_000_000u128));
|
||||
if a > b {
|
||||
assert!(a - b < epsilon, "{} != {}", a, b)
|
||||
} else {
|
||||
|
||||
@@ -1,457 +0,0 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::mixnode::{MixNodeCostParams, MixNodeRewarding};
|
||||
use crate::reward_params::{IntervalRewardParams, NodeRewardParams, RewardingParams};
|
||||
use crate::rewarding::helpers::truncate_reward;
|
||||
use crate::rewarding::RewardDistribution;
|
||||
use crate::{Delegation, Interval, Percent};
|
||||
use cosmwasm_std::{Addr, Coin, Decimal};
|
||||
|
||||
pub struct Simulator {
|
||||
pub node_rewarding_details: MixNodeRewarding,
|
||||
pub node_delegations: Vec<Delegation>,
|
||||
pub system_rewarding_params: RewardingParams,
|
||||
|
||||
pub interval: Interval,
|
||||
}
|
||||
|
||||
impl Simulator {
|
||||
pub fn new(
|
||||
profit_margin_percent: Percent,
|
||||
interval_operating_cost: Coin,
|
||||
system_rewarding_params: RewardingParams,
|
||||
initial_pledge: Coin,
|
||||
interval: Interval,
|
||||
) -> Self {
|
||||
let cost_params = MixNodeCostParams {
|
||||
profit_margin_percent,
|
||||
interval_operating_cost,
|
||||
};
|
||||
|
||||
Simulator {
|
||||
node_rewarding_details: MixNodeRewarding::initialise_new(
|
||||
cost_params,
|
||||
&initial_pledge,
|
||||
Default::default(),
|
||||
),
|
||||
node_delegations: vec![],
|
||||
system_rewarding_params,
|
||||
interval,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delegate(&mut self, amount: Coin) {
|
||||
let cumulative_reward_ratio = self.node_rewarding_details.total_unit_reward;
|
||||
// let record = self.node_rewarding_details.increment_period();
|
||||
// self.node_historical_records.insert(period, record);
|
||||
self.node_rewarding_details
|
||||
.add_base_delegation(amount.amount);
|
||||
|
||||
// we don't care about the owner/node details here
|
||||
self.node_delegations.push(Delegation::new(
|
||||
Addr::unchecked("bob"),
|
||||
42,
|
||||
cumulative_reward_ratio,
|
||||
amount,
|
||||
123,
|
||||
None,
|
||||
))
|
||||
}
|
||||
|
||||
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
|
||||
self.node_rewarding_details
|
||||
.determine_delegation_reward(delegation)
|
||||
}
|
||||
|
||||
// since this is a simulator only, not something to be used in the production code, the unwraps are fine
|
||||
// if user inputs are invalid
|
||||
pub fn undelegate(
|
||||
&mut self,
|
||||
delegation_index: usize,
|
||||
) -> Result<(Coin, Coin), MixnetContractError> {
|
||||
let delegation = self.node_delegations.remove(delegation_index);
|
||||
let reward = self.determine_delegation_reward(&delegation);
|
||||
self.node_rewarding_details
|
||||
.decrease_delegates_decimal(delegation.dec_amount() + reward)?;
|
||||
self.node_rewarding_details.unique_delegations -= 1;
|
||||
|
||||
let reward_denom = &delegation.amount.denom;
|
||||
let truncated_reward = truncate_reward(reward, reward_denom);
|
||||
|
||||
// if this was last delegation, move all leftover decimal tokens to the operator
|
||||
// (this is literally in the order of a millionth of a micronym)
|
||||
if self.node_delegations.is_empty() {
|
||||
assert_eq!(self.node_rewarding_details.unique_delegations, 0);
|
||||
self.node_rewarding_details.operator += self.node_rewarding_details.delegates;
|
||||
self.node_rewarding_details.delegates = Decimal::zero();
|
||||
}
|
||||
|
||||
Ok((delegation.amount, truncated_reward))
|
||||
}
|
||||
|
||||
pub fn determine_total_delegation_reward(&self) -> Decimal {
|
||||
let mut total = Decimal::zero();
|
||||
for delegation in &self.node_delegations {
|
||||
total += self.determine_delegation_reward(delegation)
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
pub fn simulate_epoch(&mut self, node_params: NodeRewardParams) -> RewardDistribution {
|
||||
let reward_distribution = self.node_rewarding_details.calculate_epoch_reward(
|
||||
&self.system_rewarding_params,
|
||||
node_params,
|
||||
self.interval.epochs_in_interval(),
|
||||
);
|
||||
self.node_rewarding_details.distribute_rewards(
|
||||
reward_distribution,
|
||||
self.interval.current_epoch_absolute_id(),
|
||||
);
|
||||
self.interval = self.interval.advance_epoch();
|
||||
|
||||
reward_distribution
|
||||
}
|
||||
|
||||
// assume node state doesn't change in the interval (kinda unrealistic)
|
||||
pub fn simulate_interval(&mut self, node_params: NodeRewardParams) {
|
||||
assert_eq!(self.interval.current_epoch_id(), 0);
|
||||
let id = self.interval.current_interval_id();
|
||||
let mut distributed = Decimal::zero();
|
||||
for _ in 0..self.interval.epochs_in_interval() {
|
||||
let distr = self.simulate_epoch(node_params);
|
||||
distributed += distr.operator;
|
||||
distributed += distr.delegates;
|
||||
}
|
||||
assert_eq!(id + 1, self.interval.current_interval_id());
|
||||
|
||||
// update reward pool and all of that
|
||||
let old = self.system_rewarding_params.interval;
|
||||
let reward_pool = old.reward_pool - distributed;
|
||||
let staking_supply = old.staking_supply + distributed;
|
||||
let epoch_reward_budget = reward_pool
|
||||
/ Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap()
|
||||
* old.interval_pool_emission.value();
|
||||
let stake_saturation_point = staking_supply
|
||||
/ Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap();
|
||||
|
||||
let updated_params = RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool,
|
||||
staking_supply,
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: old.sybil_resistance,
|
||||
active_set_work_factor: old.active_set_work_factor,
|
||||
interval_pool_emission: old.interval_pool_emission,
|
||||
},
|
||||
rewarded_set_size: self.system_rewarding_params.rewarded_set_size,
|
||||
active_set_size: self.system_rewarding_params.active_set_size,
|
||||
};
|
||||
|
||||
self.system_rewarding_params = updated_params;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::helpers::compare_decimals;
|
||||
use crate::reward_params::IntervalRewardParams;
|
||||
use crate::rewarding::helpers::truncate_reward_amount;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use cosmwasm_std::{coin, Uint128};
|
||||
use std::time::Duration;
|
||||
|
||||
fn base_simulator(initial_pledge: u128) -> Simulator {
|
||||
let profit_margin = Percent::from_percentage_value(10).unwrap();
|
||||
let interval_operating_cost = Coin::new(40_000_000, "unym");
|
||||
let epochs_in_interval = 720u32;
|
||||
let rewarded_set_size = 240;
|
||||
let active_set_size = 100;
|
||||
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
|
||||
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
let epoch_reward_budget =
|
||||
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
|
||||
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
|
||||
|
||||
let rewarding_params = RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::percent(1000), // value '10'
|
||||
interval_pool_emission,
|
||||
},
|
||||
rewarded_set_size,
|
||||
active_set_size,
|
||||
};
|
||||
|
||||
let interval = Interval::init_interval(
|
||||
epochs_in_interval,
|
||||
Duration::from_secs(60 * 60),
|
||||
&mock_env(),
|
||||
);
|
||||
let initial_pledge = Coin::new(initial_pledge, "unym");
|
||||
Simulator::new(
|
||||
profit_margin,
|
||||
interval_operating_cost,
|
||||
rewarding_params,
|
||||
initial_pledge,
|
||||
interval,
|
||||
)
|
||||
}
|
||||
|
||||
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
|
||||
fn check_rewarding_invariant(simulator: &Simulator) {
|
||||
let delegation_sum: Decimal = simulator
|
||||
.node_delegations
|
||||
.iter()
|
||||
.map(|d| d.dec_amount())
|
||||
.sum();
|
||||
let reward_sum = simulator.determine_total_delegation_reward();
|
||||
compare_decimals(
|
||||
delegation_sum + reward_sum,
|
||||
simulator.node_rewarding_details.delegates,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulator_returns_expected_values_for_base_case() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
|
||||
let epoch_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
let rewards = simulator.simulate_epoch(epoch_params);
|
||||
|
||||
assert_eq!(rewards.delegates, Decimal::zero());
|
||||
compare_decimals(rewards.operator, "1128452.5416104363".parse().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_delegation_at_genesis() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
simulator.delegate(Coin::new(18000_000000, "unym"));
|
||||
|
||||
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
let rewards = simulator.simulate_epoch(node_params);
|
||||
|
||||
compare_decimals(rewards.delegates, "1795950.2602660495".parse().unwrap());
|
||||
compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap());
|
||||
|
||||
compare_decimals(
|
||||
rewards.delegates,
|
||||
simulator.determine_total_delegation_reward(),
|
||||
);
|
||||
assert_eq!(
|
||||
simulator.node_rewarding_details.operator,
|
||||
rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
simulator.node_rewarding_details.delegates,
|
||||
rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegation_and_undelegation() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
|
||||
let rewards1 = simulator.simulate_epoch(node_params);
|
||||
let expected_operator1 = "1128452.5416104363".parse().unwrap();
|
||||
assert_eq!(rewards1.delegates, Decimal::zero());
|
||||
compare_decimals(rewards1.operator, expected_operator1);
|
||||
|
||||
simulator.delegate(Coin::new(18000_000000, "unym"));
|
||||
|
||||
let rewards2 = simulator.simulate_epoch(node_params);
|
||||
let expected_operator2 = "1363843.413584609".parse().unwrap();
|
||||
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward1);
|
||||
compare_decimals(rewards2.operator, expected_operator2);
|
||||
|
||||
let rewards3 = simulator.simulate_epoch(node_params);
|
||||
let expected_operator3 = "1364017.7824440491".parse().unwrap();
|
||||
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
|
||||
compare_decimals(rewards3.delegates, expected_delegator_reward2);
|
||||
compare_decimals(rewards3.operator, expected_operator3);
|
||||
|
||||
let (delegation, reward) = simulator.undelegate(0).unwrap();
|
||||
assert_eq!(delegation.amount.u128(), 18000_000000);
|
||||
assert_eq!(
|
||||
reward.amount,
|
||||
(expected_delegator_reward1 + expected_delegator_reward2) * Uint128::new(1)
|
||||
);
|
||||
|
||||
let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap();
|
||||
compare_decimals(
|
||||
simulator.node_rewarding_details.operator,
|
||||
base_op + expected_operator1 + expected_operator2 + expected_operator3,
|
||||
);
|
||||
assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn withdrawing_operator_reward() {
|
||||
// essentially all delegators' rewards (and the operator itself) are still correctly computed
|
||||
let original_pledge = coin(10000_000000, "unym");
|
||||
let mut simulator = base_simulator(original_pledge.amount.u128());
|
||||
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
|
||||
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
|
||||
// delegating at different times still work)
|
||||
simulator.delegate(Coin::new(18000_000000, "unym"));
|
||||
simulator.delegate(Coin::new(4000_000000, "unym"));
|
||||
|
||||
// "normal", sanity check rewarding
|
||||
let rewards1 = simulator.simulate_epoch(node_params);
|
||||
let expected_operator1 = "1411087.1007647323".parse().unwrap();
|
||||
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
|
||||
compare_decimals(rewards1.delegates, expected_delegator_reward1);
|
||||
compare_decimals(rewards1.operator, expected_operator1);
|
||||
check_rewarding_invariant(&simulator);
|
||||
|
||||
let reward = simulator
|
||||
.node_rewarding_details
|
||||
.withdraw_operator_reward(&original_pledge);
|
||||
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
|
||||
assert_eq!(
|
||||
simulator.node_rewarding_details.operator,
|
||||
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
|
||||
);
|
||||
|
||||
let rewards2 = simulator.simulate_epoch(node_params);
|
||||
let expected_operator2 = "1411113.0004067947".parse().unwrap();
|
||||
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward2);
|
||||
compare_decimals(rewards2.operator, expected_operator2);
|
||||
check_rewarding_invariant(&simulator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn withdrawing_delegator_reward() {
|
||||
// essentially all delegators' rewards (and the operator itself) are still correctly computed
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
let node_params = NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
|
||||
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
|
||||
// delegating at different times still work)
|
||||
simulator.delegate(Coin::new(18000_000000, "unym"));
|
||||
simulator.delegate(Coin::new(4000_000000, "unym"));
|
||||
|
||||
// "normal", sanity check rewarding
|
||||
let rewards1 = simulator.simulate_epoch(node_params);
|
||||
let expected_operator1 = "1411087.1007647323".parse().unwrap();
|
||||
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
|
||||
compare_decimals(rewards1.delegates, expected_delegator_reward1);
|
||||
compare_decimals(rewards1.operator, expected_operator1);
|
||||
check_rewarding_invariant(&simulator);
|
||||
|
||||
// reference to our `18000_000000` delegation
|
||||
let delegation1 = &mut simulator.node_delegations[0];
|
||||
let reward = simulator
|
||||
.node_rewarding_details
|
||||
.withdraw_delegator_reward(delegation1)
|
||||
.unwrap();
|
||||
let expected_del1_reward = "1799968.1174089068".parse().unwrap();
|
||||
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
|
||||
|
||||
// new reward after withdrawal
|
||||
let rewards2 = simulator.simulate_epoch(node_params);
|
||||
let expected_operator2 = "1411250.1907492676".parse().unwrap();
|
||||
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward2);
|
||||
compare_decimals(rewards2.operator, expected_operator2);
|
||||
check_rewarding_invariant(&simulator);
|
||||
|
||||
// check final values
|
||||
let reward_del1 = simulator
|
||||
.node_rewarding_details
|
||||
.withdraw_delegator_reward(&mut simulator.node_delegations[0])
|
||||
.unwrap();
|
||||
let expected_del1_reward = "1799970.5883041779".parse().unwrap();
|
||||
assert_eq!(
|
||||
reward_del1.amount,
|
||||
truncate_reward_amount(expected_del1_reward)
|
||||
);
|
||||
|
||||
let reward_del2 = simulator
|
||||
.node_rewarding_details
|
||||
.withdraw_delegator_reward(&mut simulator.node_delegations[1])
|
||||
.unwrap();
|
||||
let first: Decimal = "399992.91497975704".parse().unwrap();
|
||||
let second: Decimal = "400033.4627055114".parse().unwrap();
|
||||
let expected_del2_reward = first + second;
|
||||
assert_eq!(
|
||||
reward_del2.amount,
|
||||
truncate_reward_amount(expected_del2_reward)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulating_multiple_epochs() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
|
||||
let mut is_active = true;
|
||||
let mut performance = Percent::from_percentage_value(100).unwrap();
|
||||
for epoch in 0..720 {
|
||||
if epoch == 0 {
|
||||
simulator.delegate(Coin::new(18000_000000, "unym"))
|
||||
}
|
||||
if epoch == 42 {
|
||||
simulator.delegate(Coin::new(2000_000000, "unym"))
|
||||
}
|
||||
if epoch == 89 {
|
||||
is_active = false;
|
||||
}
|
||||
if epoch == 123 {
|
||||
simulator.delegate(Coin::new(6666_000000, "unym"))
|
||||
}
|
||||
if epoch == 167 {
|
||||
performance = Percent::from_percentage_value(90).unwrap();
|
||||
}
|
||||
if epoch == 245 {
|
||||
simulator.delegate(Coin::new(2050_000000, "unym"))
|
||||
}
|
||||
if epoch == 264 {
|
||||
let (delegation, _reward) = simulator.undelegate(1).unwrap();
|
||||
// sanity check to make sure we undelegated what we wanted to undelegate : )
|
||||
assert_eq!(delegation.amount.u128(), 2000_000000);
|
||||
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
|
||||
}
|
||||
if epoch == 345 {
|
||||
is_active = true;
|
||||
}
|
||||
if epoch == 358 {
|
||||
performance = Percent::from_percentage_value(100).unwrap();
|
||||
}
|
||||
if epoch == 458 {
|
||||
let (delegation, _reward) = simulator.undelegate(0).unwrap();
|
||||
// sanity check to make sure we undelegated what we wanted to undelegate : )
|
||||
assert_eq!(delegation.amount.u128(), 18000_000000);
|
||||
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
|
||||
}
|
||||
if epoch == 545 {
|
||||
simulator.delegate(Coin::new(5000_000000, "unym"))
|
||||
}
|
||||
|
||||
// this has to always hold
|
||||
check_rewarding_invariant(&simulator);
|
||||
let node_params = NodeRewardParams::new(performance, is_active);
|
||||
simulator.simulate_epoch(node_params);
|
||||
}
|
||||
|
||||
// after everyone undelegates, there should be nothing left in the delegates pool
|
||||
let delegations = simulator.node_delegations.len();
|
||||
for _ in 0..delegations {
|
||||
simulator.undelegate(0).unwrap();
|
||||
}
|
||||
assert_eq!(Decimal::zero(), simulator.node_rewarding_details.delegates);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,749 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::reward_params::NodeRewardParams;
|
||||
use crate::rewarding::simulator::simulated_node::SimulatedNode;
|
||||
use crate::rewarding::RewardDistribution;
|
||||
use crate::{
|
||||
Delegation, Interval, IntervalRewardParams, MixId, MixNodeCostParams, RewardingParams,
|
||||
};
|
||||
use cosmwasm_std::{Coin, Decimal};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
pub mod simulated_node;
|
||||
|
||||
pub struct Simulator {
|
||||
pub nodes: BTreeMap<MixId, SimulatedNode>,
|
||||
pub system_rewarding_params: RewardingParams,
|
||||
pub interval: Interval,
|
||||
|
||||
next_mix_id: MixId,
|
||||
pending_reward_pool_emission: Decimal,
|
||||
}
|
||||
|
||||
impl Simulator {
|
||||
pub fn new(system_rewarding_params: RewardingParams, interval: Interval) -> Self {
|
||||
Simulator {
|
||||
nodes: Default::default(),
|
||||
system_rewarding_params,
|
||||
interval,
|
||||
next_mix_id: 0,
|
||||
pending_reward_pool_emission: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn advance_epoch(&mut self) {
|
||||
let updated = self.interval.advance_epoch();
|
||||
|
||||
// we rolled over an interval
|
||||
if self.interval.current_interval_id() + 1 == updated.current_interval_id() {
|
||||
let old = self.system_rewarding_params.interval;
|
||||
let reward_pool = old.reward_pool - self.pending_reward_pool_emission;
|
||||
let staking_supply = old.staking_supply + self.pending_reward_pool_emission;
|
||||
let epoch_reward_budget = reward_pool
|
||||
/ Decimal::from_atomics(self.interval.epochs_in_interval(), 0).unwrap()
|
||||
* old.interval_pool_emission.value();
|
||||
let stake_saturation_point = staking_supply
|
||||
/ Decimal::from_atomics(self.system_rewarding_params.rewarded_set_size, 0).unwrap();
|
||||
|
||||
let updated_params = RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool,
|
||||
staking_supply,
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: old.sybil_resistance,
|
||||
active_set_work_factor: old.active_set_work_factor,
|
||||
interval_pool_emission: old.interval_pool_emission,
|
||||
},
|
||||
rewarded_set_size: self.system_rewarding_params.rewarded_set_size,
|
||||
active_set_size: self.system_rewarding_params.active_set_size,
|
||||
};
|
||||
|
||||
self.system_rewarding_params = updated_params;
|
||||
self.pending_reward_pool_emission = Decimal::zero();
|
||||
}
|
||||
self.interval = updated;
|
||||
}
|
||||
|
||||
pub fn bond(&mut self, pledge: Coin, cost_params: MixNodeCostParams) -> MixId {
|
||||
let mix_id = self.next_mix_id;
|
||||
|
||||
self.nodes.insert(
|
||||
mix_id,
|
||||
SimulatedNode::new(
|
||||
mix_id,
|
||||
cost_params,
|
||||
&pledge,
|
||||
self.interval.current_epoch_absolute_id(),
|
||||
),
|
||||
);
|
||||
|
||||
self.next_mix_id += 1;
|
||||
|
||||
mix_id
|
||||
}
|
||||
|
||||
pub fn delegate<S: Into<String>>(&mut self, delegator: S, delegation: Coin, mix_id: MixId) {
|
||||
let node = self.nodes.get_mut(&mix_id).expect("node doesn't exist");
|
||||
node.delegate(delegator, delegation)
|
||||
}
|
||||
|
||||
// since this is a simulator only, not something to be used in the production code, the unwraps are fine
|
||||
// if user inputs are invalid
|
||||
pub fn undelegate<S: Into<String>>(
|
||||
&mut self,
|
||||
delegator: S,
|
||||
mix_id: MixId,
|
||||
) -> Result<(Coin, Coin), MixnetContractError> {
|
||||
let node = self.nodes.get_mut(&mix_id).expect("node not found");
|
||||
node.undelegate(delegator)
|
||||
}
|
||||
|
||||
pub fn simulate_epoch_single_node(&mut self, params: NodeRewardParams) -> RewardDistribution {
|
||||
assert_eq!(self.nodes.len(), 1);
|
||||
|
||||
let id = *self.nodes.keys().next().unwrap();
|
||||
let mut params_map = BTreeMap::new();
|
||||
params_map.insert(id, params);
|
||||
self.simulate_epoch(¶ms_map).remove(&id).unwrap()
|
||||
}
|
||||
|
||||
pub fn simulate_epoch(
|
||||
&mut self,
|
||||
node_params: &BTreeMap<MixId, NodeRewardParams>,
|
||||
) -> BTreeMap<MixId, RewardDistribution> {
|
||||
let mut params_keys = node_params.keys().copied().collect::<Vec<_>>();
|
||||
params_keys.sort_unstable();
|
||||
let mut node_keys = self.nodes.keys().copied().collect::<Vec<_>>();
|
||||
node_keys.sort_unstable();
|
||||
|
||||
if params_keys != node_keys {
|
||||
panic!("invalid node rewarding params provided");
|
||||
}
|
||||
|
||||
let mut dist = BTreeMap::new();
|
||||
|
||||
for (mix_id, node) in self.nodes.iter_mut() {
|
||||
let reward_distribution = node.rewarding_details.calculate_epoch_reward(
|
||||
&self.system_rewarding_params,
|
||||
node_params[mix_id],
|
||||
self.interval.epochs_in_interval(),
|
||||
);
|
||||
node.rewarding_details.distribute_rewards(
|
||||
reward_distribution,
|
||||
self.interval.current_epoch_absolute_id(),
|
||||
);
|
||||
self.pending_reward_pool_emission += reward_distribution.operator;
|
||||
self.pending_reward_pool_emission += reward_distribution.delegates;
|
||||
|
||||
dist.insert(*mix_id, reward_distribution);
|
||||
}
|
||||
|
||||
self.advance_epoch();
|
||||
dist
|
||||
}
|
||||
|
||||
pub fn determine_delegation_reward(&self, delegation: &Delegation) -> Decimal {
|
||||
self.nodes[&delegation.mix_id]
|
||||
.rewarding_details
|
||||
.determine_delegation_reward(delegation)
|
||||
}
|
||||
|
||||
pub fn determine_total_delegation_reward(&self) -> Decimal {
|
||||
let mut total = Decimal::zero();
|
||||
|
||||
for node in self.nodes.values() {
|
||||
for delegation in node.delegations.values() {
|
||||
total += node
|
||||
.rewarding_details
|
||||
.determine_delegation_reward(delegation)
|
||||
}
|
||||
}
|
||||
total
|
||||
}
|
||||
|
||||
// assume node state doesn't change in the interval (kinda unrealistic)
|
||||
pub fn simulate_full_interval(&mut self, node_params: &BTreeMap<MixId, NodeRewardParams>) {
|
||||
for _ in 0..self.interval.epochs_in_interval() {
|
||||
self.simulate_epoch(node_params);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::helpers::compare_decimals;
|
||||
use crate::Percent;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use std::time::Duration;
|
||||
|
||||
#[cfg(test)]
|
||||
mod single_node_case {
|
||||
use super::*;
|
||||
use crate::helpers::compare_decimals;
|
||||
use crate::reward_params::IntervalRewardParams;
|
||||
use crate::rewarding::helpers::truncate_reward_amount;
|
||||
use crate::Percent;
|
||||
use cosmwasm_std::coin;
|
||||
use cosmwasm_std::testing::mock_env;
|
||||
use std::time::Duration;
|
||||
|
||||
fn base_simulator(initial_pledge: u128) -> Simulator {
|
||||
let profit_margin = Percent::from_percentage_value(10).unwrap();
|
||||
let interval_operating_cost = Coin::new(40_000_000, "unym");
|
||||
let epochs_in_interval = 720u32;
|
||||
let rewarded_set_size = 240;
|
||||
let active_set_size = 100;
|
||||
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
|
||||
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
let epoch_reward_budget =
|
||||
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
|
||||
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
|
||||
|
||||
let rewarding_params = RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(), // 250M * 1M (we're expressing it all in base tokens)
|
||||
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(), // 100M * 1M
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::percent(1000), // value '10'
|
||||
interval_pool_emission,
|
||||
},
|
||||
rewarded_set_size,
|
||||
active_set_size,
|
||||
};
|
||||
|
||||
let interval = Interval::init_interval(
|
||||
epochs_in_interval,
|
||||
Duration::from_secs(60 * 60),
|
||||
&mock_env(),
|
||||
);
|
||||
let initial_pledge = Coin::new(initial_pledge, "unym");
|
||||
let mut simulator = Simulator::new(rewarding_params, interval);
|
||||
|
||||
let cost_params = MixNodeCostParams {
|
||||
profit_margin_percent: profit_margin,
|
||||
interval_operating_cost,
|
||||
};
|
||||
simulator.bond(initial_pledge, cost_params);
|
||||
simulator
|
||||
}
|
||||
|
||||
// essentially our delegations + estimated rewards HAVE TO equal to what we actually determined
|
||||
fn check_rewarding_invariant(simulator: &Simulator) {
|
||||
for node in simulator.nodes.values() {
|
||||
let delegation_sum: Decimal =
|
||||
node.delegations.values().map(|d| d.dec_amount()).sum();
|
||||
|
||||
let reward_sum: Decimal = node
|
||||
.delegations
|
||||
.values()
|
||||
.map(|d| node.rewarding_details.determine_delegation_reward(d))
|
||||
.sum();
|
||||
|
||||
// let reward_sum = simulator.determine_total_delegation_reward();
|
||||
compare_decimals(
|
||||
delegation_sum + reward_sum,
|
||||
node.rewarding_details.delegates,
|
||||
None,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulator_returns_expected_values_for_base_case() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
|
||||
let epoch_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
let rewards = simulator.simulate_epoch_single_node(epoch_params);
|
||||
|
||||
assert_eq!(rewards.delegates, Decimal::zero());
|
||||
compare_decimals(
|
||||
rewards.operator,
|
||||
"1128452.5416104363".parse().unwrap(),
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_delegation_at_genesis() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
|
||||
let node_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
let rewards = simulator.simulate_epoch_single_node(node_params);
|
||||
|
||||
compare_decimals(
|
||||
rewards.delegates,
|
||||
"1795950.2602660495".parse().unwrap(),
|
||||
None,
|
||||
);
|
||||
compare_decimals(rewards.operator, "1363716.856243172".parse().unwrap(), None);
|
||||
|
||||
compare_decimals(
|
||||
rewards.delegates,
|
||||
simulator.determine_total_delegation_reward(),
|
||||
None,
|
||||
);
|
||||
let node = &simulator.nodes[&0];
|
||||
assert_eq!(
|
||||
node.rewarding_details.operator,
|
||||
rewards.operator + Decimal::from_atomics(10000_000000u128, 0).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
node.rewarding_details.delegates,
|
||||
rewards.delegates + Decimal::from_atomics(18000_000000u128, 0).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delegation_and_undelegation() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
let node_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params);
|
||||
let expected_operator1 = "1128452.5416104363".parse().unwrap();
|
||||
assert_eq!(rewards1.delegates, Decimal::zero());
|
||||
compare_decimals(rewards1.operator, expected_operator1, None);
|
||||
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params);
|
||||
let expected_operator2 = "1363843.413584609".parse().unwrap();
|
||||
let expected_delegator_reward1 = "1795952.25874404".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward1, None);
|
||||
compare_decimals(rewards2.operator, expected_operator2, None);
|
||||
|
||||
let rewards3 = simulator.simulate_epoch_single_node(node_params);
|
||||
let expected_operator3 = "1364017.7824440491".parse().unwrap();
|
||||
let expected_delegator_reward2 = "1796135.9269468693".parse().unwrap();
|
||||
compare_decimals(rewards3.delegates, expected_delegator_reward2, None);
|
||||
compare_decimals(rewards3.operator, expected_operator3, None);
|
||||
|
||||
let (delegation, reward) = simulator.undelegate("alice", 0).unwrap();
|
||||
assert_eq!(delegation.amount.u128(), 18000_000000);
|
||||
assert_eq!(
|
||||
reward.amount,
|
||||
truncate_reward_amount(expected_delegator_reward1 + expected_delegator_reward2)
|
||||
);
|
||||
|
||||
let base_op = Decimal::from_atomics(10000_000000u128, 0).unwrap();
|
||||
|
||||
let node = &simulator.nodes[&0];
|
||||
compare_decimals(
|
||||
node.rewarding_details.operator,
|
||||
base_op + expected_operator1 + expected_operator2 + expected_operator3,
|
||||
None,
|
||||
);
|
||||
assert_eq!(Decimal::zero(), node.rewarding_details.delegates);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn withdrawing_operator_reward() {
|
||||
// essentially all delegators' rewards (and the operator itself) are still correctly computed
|
||||
let original_pledge = coin(10000_000000, "unym");
|
||||
let mut simulator = base_simulator(original_pledge.amount.u128());
|
||||
let node_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
|
||||
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
|
||||
// delegating at different times still work)
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0);
|
||||
|
||||
// "normal", sanity check rewarding
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params);
|
||||
let expected_operator1 = "1411087.1007647323".parse().unwrap();
|
||||
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
|
||||
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
|
||||
compare_decimals(rewards1.operator, expected_operator1, None);
|
||||
check_rewarding_invariant(&simulator);
|
||||
|
||||
let node = simulator.nodes.get_mut(&0).unwrap();
|
||||
let reward = node
|
||||
.rewarding_details
|
||||
.withdraw_operator_reward(&original_pledge);
|
||||
assert_eq!(reward.amount, truncate_reward_amount(expected_operator1));
|
||||
assert_eq!(
|
||||
node.rewarding_details.operator,
|
||||
Decimal::from_atomics(original_pledge.amount, 0).unwrap()
|
||||
);
|
||||
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params);
|
||||
let expected_operator2 = "1411113.0004067947".parse().unwrap();
|
||||
let expected_delegator_reward2 = "2200183.3879084454".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
|
||||
compare_decimals(rewards2.operator, expected_operator2, None);
|
||||
check_rewarding_invariant(&simulator);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn withdrawing_delegator_reward() {
|
||||
// essentially all delegators' rewards (and the operator itself) are still correctly computed
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
let node_params =
|
||||
NodeRewardParams::new(Percent::from_percentage_value(100).unwrap(), true);
|
||||
|
||||
// add 2 delegations at genesis (because it makes things easier and as shown with previous tests
|
||||
// delegating at different times still work)
|
||||
simulator.delegate("alice", Coin::new(18000_000000, "unym"), 0);
|
||||
simulator.delegate("bob", Coin::new(4000_000000, "unym"), 0);
|
||||
|
||||
// "normal", sanity check rewarding
|
||||
let rewards1 = simulator.simulate_epoch_single_node(node_params);
|
||||
let expected_operator1 = "1411087.1007647323".parse().unwrap();
|
||||
let expected_delegator_reward1 = "2199961.032388664".parse().unwrap();
|
||||
compare_decimals(rewards1.delegates, expected_delegator_reward1, None);
|
||||
compare_decimals(rewards1.operator, expected_operator1, None);
|
||||
check_rewarding_invariant(&simulator);
|
||||
|
||||
// reference to our `18000_000000` delegation
|
||||
let node = simulator.nodes.get_mut(&0).unwrap();
|
||||
let delegation1 = node.delegations.get_mut("alice").unwrap();
|
||||
let reward = node
|
||||
.rewarding_details
|
||||
.withdraw_delegator_reward(delegation1)
|
||||
.unwrap();
|
||||
let expected_del1_reward = "1799968.1174089068".parse().unwrap();
|
||||
assert_eq!(reward.amount, truncate_reward_amount(expected_del1_reward));
|
||||
|
||||
// new reward after withdrawal
|
||||
let rewards2 = simulator.simulate_epoch_single_node(node_params);
|
||||
let expected_operator2 = "1411250.1907492676".parse().unwrap();
|
||||
let expected_delegator_reward2 = "2200004.051009689".parse().unwrap();
|
||||
compare_decimals(rewards2.delegates, expected_delegator_reward2, None);
|
||||
compare_decimals(rewards2.operator, expected_operator2, None);
|
||||
check_rewarding_invariant(&simulator);
|
||||
|
||||
// check final values
|
||||
let node = simulator.nodes.get_mut(&0).unwrap();
|
||||
let delegation1 = node.delegations.get_mut("alice").unwrap();
|
||||
|
||||
let reward_del1 = node
|
||||
.rewarding_details
|
||||
.withdraw_delegator_reward(delegation1)
|
||||
.unwrap();
|
||||
let expected_del1_reward = "1799970.5883041779".parse().unwrap();
|
||||
assert_eq!(
|
||||
reward_del1.amount,
|
||||
truncate_reward_amount(expected_del1_reward)
|
||||
);
|
||||
|
||||
let delegation2 = node.delegations.get_mut("bob").unwrap();
|
||||
let reward_del2 = node
|
||||
.rewarding_details
|
||||
.withdraw_delegator_reward(delegation2)
|
||||
.unwrap();
|
||||
let first: Decimal = "399992.91497975704".parse().unwrap();
|
||||
let second: Decimal = "400033.4627055114".parse().unwrap();
|
||||
let expected_del2_reward = first + second;
|
||||
assert_eq!(
|
||||
reward_del2.amount,
|
||||
truncate_reward_amount(expected_del2_reward)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn simulating_multiple_epochs() {
|
||||
let mut simulator = base_simulator(10000_000000);
|
||||
|
||||
let mut is_active = true;
|
||||
let mut performance = Percent::from_percentage_value(100).unwrap();
|
||||
for epoch in 0..720 {
|
||||
if epoch == 0 {
|
||||
simulator.delegate("a", Coin::new(18000_000000, "unym"), 0)
|
||||
}
|
||||
if epoch == 42 {
|
||||
simulator.delegate("b", Coin::new(2000_000000, "unym"), 0)
|
||||
}
|
||||
if epoch == 89 {
|
||||
is_active = false;
|
||||
}
|
||||
if epoch == 123 {
|
||||
simulator.delegate("c", Coin::new(6666_000000, "unym"), 0)
|
||||
}
|
||||
if epoch == 167 {
|
||||
performance = Percent::from_percentage_value(90).unwrap();
|
||||
}
|
||||
if epoch == 245 {
|
||||
simulator.delegate("d", Coin::new(2050_000000, "unym"), 0)
|
||||
}
|
||||
if epoch == 264 {
|
||||
let (delegation, _reward) = simulator.undelegate("b", 0).unwrap();
|
||||
// sanity check to make sure we undelegated what we wanted to undelegate : )
|
||||
assert_eq!(delegation.amount.u128(), 2000_000000);
|
||||
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
|
||||
}
|
||||
if epoch == 345 {
|
||||
is_active = true;
|
||||
}
|
||||
if epoch == 358 {
|
||||
performance = Percent::from_percentage_value(100).unwrap();
|
||||
}
|
||||
if epoch == 458 {
|
||||
let (delegation, _reward) = simulator.undelegate("a", 0).unwrap();
|
||||
// sanity check to make sure we undelegated what we wanted to undelegate : )
|
||||
assert_eq!(delegation.amount.u128(), 18000_000000);
|
||||
// TODO: figure out if there's a good way to verify whether `reward` is what we expect it to be
|
||||
}
|
||||
if epoch == 545 {
|
||||
simulator.delegate("e", Coin::new(5000_000000, "unym"), 0)
|
||||
}
|
||||
|
||||
// this has to always hold
|
||||
check_rewarding_invariant(&simulator);
|
||||
let node_params = NodeRewardParams::new(performance, is_active);
|
||||
simulator.simulate_epoch_single_node(node_params);
|
||||
}
|
||||
|
||||
// after everyone undelegates, there should be nothing left in the delegates pool
|
||||
simulator.undelegate("c", 0).unwrap();
|
||||
simulator.undelegate("d", 0).unwrap();
|
||||
simulator.undelegate("e", 0).unwrap();
|
||||
|
||||
let node = &simulator.nodes[&0];
|
||||
assert_eq!(Decimal::zero(), node.rewarding_details.delegates);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn multiple_nodes_against_known_values() {
|
||||
#![allow(clippy::inconsistent_digit_grouping)]
|
||||
|
||||
// TODO: this test can be further improved by checking values after EVERY interval
|
||||
// rather than just checking the final results
|
||||
|
||||
let epochs_in_interval = 1u32;
|
||||
let rewarded_set_size = 10;
|
||||
let active_set_size = 6;
|
||||
let interval_pool_emission = Percent::from_percentage_value(2).unwrap();
|
||||
|
||||
let reward_pool = 250_000_000_000_000u128;
|
||||
let staking_supply = 100_000_000_000_000u128;
|
||||
let epoch_reward_budget =
|
||||
interval_pool_emission * Decimal::from_ratio(reward_pool, epochs_in_interval);
|
||||
let stake_saturation_point = Decimal::from_ratio(staking_supply, rewarded_set_size);
|
||||
|
||||
let rewarding_params = RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(reward_pool, 0).unwrap(),
|
||||
staking_supply: Decimal::from_atomics(staking_supply, 0).unwrap(),
|
||||
epoch_reward_budget,
|
||||
stake_saturation_point,
|
||||
sybil_resistance: Percent::from_percentage_value(30).unwrap(),
|
||||
active_set_work_factor: Decimal::percent(1000), // value '10'
|
||||
interval_pool_emission,
|
||||
},
|
||||
rewarded_set_size,
|
||||
active_set_size,
|
||||
};
|
||||
|
||||
let interval = Interval::init_interval(
|
||||
epochs_in_interval,
|
||||
Duration::from_secs(60 * 60),
|
||||
&mock_env(),
|
||||
);
|
||||
|
||||
let mut simulator = Simulator::new(rewarding_params, interval);
|
||||
|
||||
let n0 = simulator.bond(
|
||||
Coin::new(11_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n0);
|
||||
|
||||
let n1 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(11_000_000_000000, "unym"), n1);
|
||||
|
||||
let n2 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n2);
|
||||
|
||||
let n3 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
|
||||
interval_operating_cost: Coin::new(500_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n3);
|
||||
|
||||
let n4 = simulator.bond(
|
||||
Coin::new(1000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_999_000_000000, "unym"), n4);
|
||||
|
||||
let n5 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n5);
|
||||
|
||||
let n6 = simulator.bond(
|
||||
Coin::new(11_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(1_000_000_000000, "unym"), n6);
|
||||
|
||||
let n7 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(9_000_000_000000, "unym"), n7);
|
||||
|
||||
let n8 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(0).unwrap(),
|
||||
interval_operating_cost: Coin::new(500_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n8);
|
||||
|
||||
let n9 = simulator.bond(
|
||||
Coin::new(1_000_000_000000, "unym"),
|
||||
MixNodeCostParams {
|
||||
profit_margin_percent: Percent::from_percentage_value(10).unwrap(),
|
||||
interval_operating_cost: Coin::new(40_000_000, "unym"),
|
||||
},
|
||||
);
|
||||
simulator.delegate("delegator", Coin::new(7_000_000_000000, "unym"), n9);
|
||||
|
||||
let uptime_1 = Percent::from_percentage_value(100).unwrap();
|
||||
let uptime_09 = Percent::from_percentage_value(90).unwrap();
|
||||
let uptime_0 = Percent::from_percentage_value(0).unwrap();
|
||||
|
||||
let node_params = [
|
||||
(n0, NodeRewardParams::new(uptime_1, true)),
|
||||
(n1, NodeRewardParams::new(uptime_1, true)),
|
||||
(n2, NodeRewardParams::new(uptime_1, true)),
|
||||
(n3, NodeRewardParams::new(uptime_09, true)),
|
||||
(n4, NodeRewardParams::new(uptime_09, true)),
|
||||
(n5, NodeRewardParams::new(uptime_0, true)),
|
||||
(n6, NodeRewardParams::new(uptime_1, false)),
|
||||
(n7, NodeRewardParams::new(uptime_1, false)),
|
||||
(n8, NodeRewardParams::new(uptime_09, false)),
|
||||
(n9, NodeRewardParams::new(uptime_0, false)),
|
||||
]
|
||||
.into_iter()
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
for _ in 0..23 {
|
||||
simulator.simulate_full_interval(&node_params);
|
||||
}
|
||||
|
||||
// we allow the delta to be within 0.1unym,
|
||||
// because the expected values, especially for the reward pool,
|
||||
// do not provide us with any higher precision
|
||||
let epsilon = Some(Decimal::from_ratio(1u32, 10u32));
|
||||
|
||||
let expected_reward_pool = "184876811322111.7".parse().unwrap();
|
||||
let expected_staking_supply = "165123188677888.3".parse().unwrap();
|
||||
compare_decimals(
|
||||
expected_reward_pool,
|
||||
simulator.system_rewarding_params.interval.reward_pool,
|
||||
epsilon,
|
||||
);
|
||||
compare_decimals(
|
||||
expected_staking_supply,
|
||||
simulator.system_rewarding_params.interval.staking_supply,
|
||||
epsilon,
|
||||
);
|
||||
|
||||
let expected_n0_pledge = "24307061704726.808".parse().unwrap();
|
||||
let expected_n0_delegated = "2031528592775.6752".parse().unwrap();
|
||||
let node = &simulator.nodes[&0].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n0_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n0_delegated, epsilon);
|
||||
|
||||
let expected_n1_pledge = "3544171010629.92".parse().unwrap();
|
||||
let expected_n1_delegated = "20854154351479.96".parse().unwrap();
|
||||
let node = &simulator.nodes[&1].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n1_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n1_delegated, epsilon);
|
||||
|
||||
let expected_n2_pledge = "3781120900089.8865".parse().unwrap();
|
||||
let expected_n2_delegated = "18634530734287.53".parse().unwrap();
|
||||
let node = &simulator.nodes[&2].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n2_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n2_delegated, epsilon);
|
||||
|
||||
let expected_n3_pledge = "2313562111772.3165".parse().unwrap();
|
||||
let expected_n3_delegated = "16090515100131.858".parse().unwrap();
|
||||
let node = &simulator.nodes[&3].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n3_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n3_delegated, epsilon);
|
||||
|
||||
let expected_n4_pledge = "1419679306492.7962".parse().unwrap();
|
||||
let expected_n4_delegated = "16802494863659.93".parse().unwrap();
|
||||
let node = &simulator.nodes[&4].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n4_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n4_delegated, epsilon);
|
||||
|
||||
let expected_n5_pledge = "1000000000000".parse().unwrap();
|
||||
let expected_n5_delegated = "7000000000000".parse().unwrap();
|
||||
let node = &simulator.nodes[&5].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n5_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n5_delegated, epsilon);
|
||||
|
||||
let expected_n6_pledge = "14114996375922.574".parse().unwrap();
|
||||
let expected_n6_delegated = "1249173915284.053".parse().unwrap();
|
||||
let node = &simulator.nodes[&6].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n6_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n6_delegated, epsilon);
|
||||
|
||||
let expected_n7_pledge = "1225564192694.3525".parse().unwrap();
|
||||
let expected_n7_delegated = "9931461332688.53".parse().unwrap();
|
||||
let node = &simulator.nodes[&7].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n7_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n7_delegated, epsilon);
|
||||
|
||||
let expected_n8_pledge = "1112319106593.8608".parse().unwrap();
|
||||
let expected_n8_delegated = "7710855078658.264".parse().unwrap();
|
||||
let node = &simulator.nodes[&8].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n8_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n8_delegated, epsilon);
|
||||
|
||||
let expected_n9_pledge = "1000000000000".parse().unwrap();
|
||||
let expected_n9_delegated = "7000000000000".parse().unwrap();
|
||||
let node = &simulator.nodes[&9].rewarding_details;
|
||||
compare_decimals(node.operator, expected_n9_pledge, epsilon);
|
||||
compare_decimals(node.delegates, expected_n9_delegated, epsilon);
|
||||
}
|
||||
}
|
||||
+73
@@ -0,0 +1,73 @@
|
||||
// Copyright 2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::{Delegation, EpochId, MixId, MixNodeCostParams, MixNodeRewarding};
|
||||
use cosmwasm_std::{Addr, Coin};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::error::MixnetContractError;
|
||||
use crate::rewarding::helpers::truncate_reward;
|
||||
|
||||
pub struct SimulatedNode {
|
||||
pub mix_id: MixId,
|
||||
pub rewarding_details: MixNodeRewarding,
|
||||
pub delegations: HashMap<String, Delegation>,
|
||||
}
|
||||
|
||||
impl SimulatedNode {
|
||||
pub fn new(
|
||||
mix_id: MixId,
|
||||
cost_params: MixNodeCostParams,
|
||||
initial_pledge: &Coin,
|
||||
current_epoch: EpochId,
|
||||
) -> Self {
|
||||
SimulatedNode {
|
||||
mix_id,
|
||||
rewarding_details: MixNodeRewarding::initialise_new(
|
||||
cost_params,
|
||||
initial_pledge,
|
||||
current_epoch,
|
||||
),
|
||||
delegations: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn delegate<S: Into<String>>(&mut self, delegator: S, delegation: Coin) {
|
||||
self.rewarding_details
|
||||
.add_base_delegation(delegation.amount);
|
||||
|
||||
let delegator = delegator.into();
|
||||
let delegation = Delegation::new(
|
||||
Addr::unchecked(&delegator),
|
||||
self.mix_id,
|
||||
self.rewarding_details.total_unit_reward,
|
||||
delegation,
|
||||
42,
|
||||
None,
|
||||
);
|
||||
|
||||
self.delegations.insert(delegator, delegation);
|
||||
}
|
||||
|
||||
pub fn undelegate<S: Into<String>>(
|
||||
&mut self,
|
||||
delegator: S,
|
||||
) -> Result<(Coin, Coin), MixnetContractError> {
|
||||
let delegator = delegator.into();
|
||||
let delegation = self
|
||||
.delegations
|
||||
.remove(&delegator)
|
||||
.expect("delegation not found");
|
||||
|
||||
let reward = self
|
||||
.rewarding_details
|
||||
.determine_delegation_reward(&delegation);
|
||||
self.rewarding_details
|
||||
.remove_delegation_decimal(delegation.dec_amount() + reward)?;
|
||||
|
||||
let reward_denom = &delegation.amount.denom;
|
||||
let truncated_reward = truncate_reward(reward, reward_denom);
|
||||
|
||||
Ok((delegation.amount, truncated_reward))
|
||||
}
|
||||
}
|
||||
@@ -739,7 +739,7 @@ pub mod tests {
|
||||
performance,
|
||||
in_active_set: true,
|
||||
};
|
||||
let sim_res = sim.simulate_epoch(node_params);
|
||||
let sim_res = sim.simulate_epoch_single_node(node_params);
|
||||
assert_eq!(sim_res, dist);
|
||||
}
|
||||
test.skip_to_next_epoch_end();
|
||||
@@ -763,7 +763,7 @@ pub mod tests {
|
||||
performance,
|
||||
in_active_set: true,
|
||||
};
|
||||
let sim_res = sim.simulate_epoch(node_params);
|
||||
let sim_res = sim.simulate_epoch_single_node(node_params);
|
||||
assert_eq!(sim_res, dist);
|
||||
}
|
||||
test.skip_to_next_epoch_end();
|
||||
@@ -804,8 +804,8 @@ pub mod tests {
|
||||
in_active_set: true,
|
||||
};
|
||||
|
||||
let dist1 = sim1.simulate_epoch(node_params);
|
||||
let dist2 = sim2.simulate_epoch(node_params);
|
||||
let dist1 = sim1.simulate_epoch_single_node(node_params);
|
||||
let dist2 = sim2.simulate_epoch_single_node(node_params);
|
||||
|
||||
let env = test.env();
|
||||
|
||||
@@ -871,6 +871,7 @@ pub mod tests {
|
||||
compare_decimals(
|
||||
computed_del11_reward + computed_del12_reward,
|
||||
delegates_reward1,
|
||||
None,
|
||||
);
|
||||
|
||||
let res2 = try_reward_mixnode(
|
||||
@@ -943,8 +944,8 @@ pub mod tests {
|
||||
in_active_set: true,
|
||||
};
|
||||
|
||||
let dist1 = sim1.simulate_epoch(node_params);
|
||||
let dist2 = sim2.simulate_epoch(node_params);
|
||||
let dist1 = sim1.simulate_epoch_single_node(node_params);
|
||||
let dist2 = sim2.simulate_epoch_single_node(node_params);
|
||||
|
||||
let env = test.env();
|
||||
|
||||
@@ -1017,6 +1018,7 @@ pub mod tests {
|
||||
compare_decimals(
|
||||
computed_del11_reward + computed_del12_reward + computed_del13_reward,
|
||||
delegates_reward1,
|
||||
None,
|
||||
);
|
||||
|
||||
let res2 = try_reward_mixnode(
|
||||
@@ -1077,6 +1079,7 @@ pub mod tests {
|
||||
compare_decimals(
|
||||
computed_del21_reward + computed_del23_reward,
|
||||
delegates_reward2,
|
||||
None,
|
||||
);
|
||||
|
||||
test.skip_to_next_epoch_end();
|
||||
|
||||
@@ -49,6 +49,7 @@ pub mod test_helpers {
|
||||
use mixnet_contract_common::mixnode::{MixNodeRewarding, UnbondedMixnode};
|
||||
use mixnet_contract_common::pending_events::{PendingEpochEventData, PendingIntervalEventData};
|
||||
use mixnet_contract_common::reward_params::{Performance, RewardingParams};
|
||||
use mixnet_contract_common::rewarding::simulator::simulated_node::SimulatedNode;
|
||||
use mixnet_contract_common::rewarding::simulator::Simulator;
|
||||
use mixnet_contract_common::rewarding::RewardDistribution;
|
||||
use mixnet_contract_common::{
|
||||
@@ -337,7 +338,7 @@ pub mod test_helpers {
|
||||
}
|
||||
|
||||
pub fn instantiate_simulator(&self, node: MixId) -> Simulator {
|
||||
simulator_from_state(self.deps(), node)
|
||||
simulator_from_single_node_state(self.deps(), node)
|
||||
}
|
||||
|
||||
pub fn execute_all_pending_events(&mut self) {
|
||||
@@ -416,7 +417,7 @@ pub mod test_helpers {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn simulator_from_state(deps: Deps<'_>, node: MixId) -> Simulator {
|
||||
pub fn simulator_from_single_node_state(deps: Deps<'_>, node: MixId) -> Simulator {
|
||||
let mix_rewarding = rewards_storage::MIXNODE_REWARDING
|
||||
.load(deps.storage, node)
|
||||
.unwrap();
|
||||
@@ -432,12 +433,21 @@ pub mod test_helpers {
|
||||
.load(deps.storage)
|
||||
.unwrap();
|
||||
let interval = interval_storage::current_interval(deps.storage).unwrap();
|
||||
Simulator {
|
||||
node_rewarding_details: mix_rewarding,
|
||||
node_delegations: delegations.delegations,
|
||||
system_rewarding_params: rewarding_params,
|
||||
interval,
|
||||
}
|
||||
let mut simulator = Simulator::new(rewarding_params, interval);
|
||||
simulator.nodes.insert(
|
||||
0,
|
||||
SimulatedNode {
|
||||
mix_id: 0,
|
||||
rewarding_details: mix_rewarding,
|
||||
delegations: delegations
|
||||
.delegations
|
||||
.into_iter()
|
||||
.map(|d| (d.owner.to_string(), d))
|
||||
.collect(),
|
||||
},
|
||||
);
|
||||
|
||||
simulator
|
||||
}
|
||||
|
||||
pub fn get_bank_send_msg(response: &Response) -> Option<(String, Vec<Coin>)> {
|
||||
|
||||
Reference in New Issue
Block a user