bugfix: manually calculate per node work on rewarded set changes (#5972)
This commit is contained in:
committed by
GitHub
parent
a9c56ef9ac
commit
17708cdf92
@@ -86,6 +86,25 @@ impl IntervalRewardParams {
|
||||
pub fn to_inline_json(&self) -> String {
|
||||
to_json_string(self).unwrap_or_else(|_| "serialisation failure".into())
|
||||
}
|
||||
|
||||
pub fn active_node_work(&self, standby_node_work: Decimal) -> WorkFactor {
|
||||
self.active_set_work_factor * standby_node_work
|
||||
}
|
||||
|
||||
pub fn standby_node_work(
|
||||
&self,
|
||||
rewarded_set_size: Decimal,
|
||||
standby_set_size: Decimal,
|
||||
) -> WorkFactor {
|
||||
let f = self.active_set_work_factor;
|
||||
let k = rewarded_set_size;
|
||||
let one = Decimal::one();
|
||||
|
||||
// nodes in reserve
|
||||
let k_r = standby_set_size;
|
||||
|
||||
one / (f * k - (f - one) * k_r)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parameters used for reward calculation.
|
||||
@@ -109,18 +128,15 @@ pub struct RewardingParams {
|
||||
|
||||
impl RewardingParams {
|
||||
pub fn active_node_work(&self) -> WorkFactor {
|
||||
self.interval.active_set_work_factor * self.standby_node_work()
|
||||
let standby_work = self.standby_node_work();
|
||||
self.interval.active_node_work(standby_work)
|
||||
}
|
||||
|
||||
pub fn standby_node_work(&self) -> WorkFactor {
|
||||
let f = self.interval.active_set_work_factor;
|
||||
let k = self.dec_rewarded_set_size();
|
||||
let one = Decimal::one();
|
||||
|
||||
// nodes in reserve
|
||||
let k_r = self.dec_standby_set_size();
|
||||
|
||||
one / (f * k - (f - one) * k_r)
|
||||
let rewarded_set_size = self.dec_rewarded_set_size();
|
||||
let standby_set_size = self.dec_standby_set_size();
|
||||
self.interval
|
||||
.standby_node_work(rewarded_set_size, standby_set_size)
|
||||
}
|
||||
|
||||
pub fn rewarded_set_size(&self) -> u32 {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
use crate::config_score::{ConfigScoreParams, OutdatedVersionWeights, VersionScoreFormulaParams};
|
||||
use crate::nym_node::Role;
|
||||
use crate::reward_params::RewardedSetParams;
|
||||
use crate::EpochId;
|
||||
use contracts_common::Percent;
|
||||
use cosmwasm_schema::cw_serde;
|
||||
@@ -85,7 +86,11 @@ impl RewardedSet {
|
||||
}
|
||||
|
||||
pub fn rewarded_set_size(&self) -> usize {
|
||||
self.active_set_size() + self.standby.len()
|
||||
self.active_set_size() + self.standby_set_size()
|
||||
}
|
||||
|
||||
pub fn standby_set_size(&self) -> usize {
|
||||
self.standby.len()
|
||||
}
|
||||
|
||||
pub fn get_role(&self, node_id: NodeId) -> Option<Role> {
|
||||
@@ -110,6 +115,13 @@ impl RewardedSet {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn matches_parameters(&self, params: RewardedSetParams) -> bool {
|
||||
self.entry_gateways.len() <= params.entry_gateways as usize
|
||||
&& self.exit_gateways.len() <= params.exit_gateways as usize
|
||||
&& self.layer1.len() + self.layer2.len() + self.layer3.len() <= params.mixnodes as usize
|
||||
&& self.standby.len() <= params.standby as usize
|
||||
}
|
||||
}
|
||||
|
||||
#[cw_serde]
|
||||
|
||||
@@ -5,12 +5,16 @@ use crate::epoch_operations::EpochAdvancer;
|
||||
use crate::support::caching::Cache;
|
||||
use cosmwasm_std::{Decimal, Fraction};
|
||||
use nym_api_requests::models::NodeAnnotation;
|
||||
use nym_mixnet_contract_common::helpers::IntoBaseDecimal;
|
||||
use nym_mixnet_contract_common::reward_params::{NodeRewardingParameters, Performance, WorkFactor};
|
||||
use nym_mixnet_contract_common::{EpochRewardedSet, ExecuteMsg, NodeId, RewardingParams};
|
||||
use nym_mixnet_contract_common::{
|
||||
EpochRewardedSet, ExecuteMsg, NodeId, RewardedSet, RewardingParams,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::cmp::max;
|
||||
use std::collections::HashMap;
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
use tracing::error;
|
||||
use tracing::{debug, error};
|
||||
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
pub(crate) struct NodeWithPerformance {
|
||||
@@ -73,6 +77,137 @@ pub(super) fn stake_to_f64(stake: Decimal) -> f64 {
|
||||
}
|
||||
}
|
||||
|
||||
struct PerNodeWork {
|
||||
active: WorkFactor,
|
||||
standby: WorkFactor,
|
||||
}
|
||||
|
||||
struct NodeWorkCalculationComponents {
|
||||
active_set_size: Decimal,
|
||||
standby_set_size: Decimal,
|
||||
per_node_work: PerNodeWork,
|
||||
}
|
||||
|
||||
impl NodeWorkCalculationComponents {
|
||||
fn standby_set_work_share(&self) -> Decimal {
|
||||
self.standby_set_size * self.per_node_work.standby
|
||||
}
|
||||
|
||||
fn active_set_work_share(&self) -> Decimal {
|
||||
self.active_set_size * self.per_node_work.active
|
||||
}
|
||||
}
|
||||
|
||||
fn default_node_work_calculation(
|
||||
nodes: &RewardedSet,
|
||||
global_rewarding_params: RewardingParams,
|
||||
) -> NodeWorkCalculationComponents {
|
||||
let per_node_work = PerNodeWork {
|
||||
active: global_rewarding_params.active_node_work(),
|
||||
standby: global_rewarding_params.standby_node_work(),
|
||||
};
|
||||
// SANITY CHECK:
|
||||
// SAFETY: 0 decimal places is within the range of `Decimal`
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let standby_set_size = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let active_set_size = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap();
|
||||
|
||||
NodeWorkCalculationComponents {
|
||||
active_set_size,
|
||||
standby_set_size,
|
||||
per_node_work,
|
||||
}
|
||||
}
|
||||
|
||||
fn manual_node_work_calculation(
|
||||
nodes: &RewardedSet,
|
||||
global_rewarding_params: RewardingParams,
|
||||
) -> NodeWorkCalculationComponents {
|
||||
// calculate everything manually based on the actual rewarded set on hand
|
||||
// but always attempt to minimise the node work, so take the maximum values
|
||||
// of the set sizes between new and old parameters
|
||||
// (more nodes = smaller per-node work as it has to be spread through more entries)
|
||||
let rewarded_set_size = max(
|
||||
global_rewarding_params.rewarded_set.rewarded_set_size(),
|
||||
nodes.rewarded_set_size() as u32,
|
||||
);
|
||||
let standby_set_size = max(
|
||||
global_rewarding_params.rewarded_set.standby,
|
||||
nodes.standby_set_size() as u32,
|
||||
);
|
||||
// the unwraps here are fine as we're guaranteed an `u32` is going to fit in a Decimal with 0 decimal places
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let rewarded_set_size_dec = rewarded_set_size.into_base_decimal().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let standby_set_size_dec = standby_set_size.into_base_decimal().unwrap();
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let active_set_size = rewarded_set_size
|
||||
.saturating_sub(standby_set_size)
|
||||
.into_base_decimal()
|
||||
.unwrap();
|
||||
|
||||
let standby_node_work = global_rewarding_params
|
||||
.interval
|
||||
.standby_node_work(rewarded_set_size_dec, standby_set_size_dec);
|
||||
let active_node_work = global_rewarding_params
|
||||
.interval
|
||||
.active_node_work(standby_node_work);
|
||||
let per_node_work = PerNodeWork {
|
||||
active: active_node_work,
|
||||
standby: standby_node_work,
|
||||
};
|
||||
|
||||
NodeWorkCalculationComponents {
|
||||
active_set_size,
|
||||
standby_set_size: standby_set_size_dec,
|
||||
per_node_work,
|
||||
}
|
||||
}
|
||||
|
||||
fn determine_per_node_work(
|
||||
nodes: &RewardedSet,
|
||||
// we only need reward parameters for active set work factor and rewarded/active set sizes;
|
||||
// we do not need exact values of reward pool, staking supply, etc., so it's fine if it's slightly out of sync
|
||||
global_rewarding_params: RewardingParams,
|
||||
) -> PerNodeWork {
|
||||
// currently we are using constant omega for nodes, but that will change with tickets
|
||||
// or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere
|
||||
let res = if nodes.matches_parameters(global_rewarding_params.rewarded_set) {
|
||||
default_node_work_calculation(nodes, global_rewarding_params)
|
||||
} else {
|
||||
error!("the current rewarded set does not much current rewarding parameters. this could only be expected if rewarded set distribution has been changed mid-epoch");
|
||||
manual_node_work_calculation(nodes, global_rewarding_params)
|
||||
};
|
||||
|
||||
let active_node_work_factor = res.per_node_work.active;
|
||||
let standby_node_work_factor = res.per_node_work.standby;
|
||||
|
||||
debug!("using {active_node_work_factor} as active node work factor and {standby_node_work_factor} as standby node work factor");
|
||||
|
||||
let standby_share = res.standby_set_work_share();
|
||||
let active_share = res.active_set_work_share();
|
||||
let total_work = standby_share + active_share;
|
||||
|
||||
// this HAS TO blow up. there's no recovery
|
||||
#[allow(clippy::panic)]
|
||||
if total_work > Decimal::one() {
|
||||
panic!("work calculation logic is flawed! somehow the total work in the system is greater than 1! \
|
||||
total work={total_work}, \
|
||||
active set share={active_share}, \
|
||||
standby share={standby_share}, \
|
||||
active node work factor={active_node_work_factor}, \
|
||||
standby node work factor={standby_node_work_factor}, \
|
||||
active set size={} \
|
||||
standby set size={}", res.active_set_size, res.standby_set_size);
|
||||
}
|
||||
|
||||
PerNodeWork {
|
||||
active: active_node_work_factor,
|
||||
standby: standby_node_work_factor,
|
||||
}
|
||||
}
|
||||
|
||||
impl EpochAdvancer {
|
||||
fn load_performance(
|
||||
status_cache: &Option<RwLockReadGuard<'_, Cache<HashMap<NodeId, NodeAnnotation>>>>,
|
||||
@@ -99,23 +234,9 @@ impl EpochAdvancer {
|
||||
global_rewarding_params: RewardingParams,
|
||||
) -> Vec<RewardedNodeWithParams> {
|
||||
let nodes = &nodes.assignment;
|
||||
// currently we are using constant omega for nodes, but that will change with tickets
|
||||
// or different reward split between entry, exit, etc. at that point this will have to be calculated elsewhere
|
||||
let active_node_work_factor = global_rewarding_params.active_node_work();
|
||||
let standby_node_work_factor = global_rewarding_params.standby_node_work();
|
||||
|
||||
// SANITY CHECK:
|
||||
// SAFETY: 0 decimal places is within the range of `Decimal`
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let standby_share = Decimal::from_atomics(nodes.standby.len() as u128, 0).unwrap()
|
||||
* standby_node_work_factor;
|
||||
#[allow(clippy::unwrap_used)]
|
||||
let active_share = Decimal::from_atomics(nodes.active_set_size() as u128, 0).unwrap()
|
||||
* active_node_work_factor;
|
||||
let total_work = standby_share + active_share;
|
||||
|
||||
// this HAS TO blow up. there's no recovery
|
||||
assert!(total_work <= Decimal::one(), "work calculation logic is flawed! somehow the total work in the system is greater than 1!");
|
||||
let nodes_work = determine_per_node_work(nodes, global_rewarding_params);
|
||||
let active_node_work_factor = nodes_work.active;
|
||||
let standby_node_work_factor = nodes_work.standby;
|
||||
|
||||
let status_cache = self.status_cache.node_annotations().await;
|
||||
if status_cache.is_none() {
|
||||
@@ -161,6 +282,9 @@ impl EpochAdvancer {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use nym_contracts_common::Percent;
|
||||
use nym_mixnet_contract_common::reward_params::RewardedSetParams;
|
||||
use nym_mixnet_contract_common::IntervalRewardParams;
|
||||
|
||||
fn compare_large_floats(a: f64, b: f64) {
|
||||
// for very large floats, allow for smaller larger epsilon
|
||||
@@ -206,4 +330,72 @@ mod tests {
|
||||
compare_large_floats(expected_f64, stake_to_f64(decimal))
|
||||
}
|
||||
}
|
||||
|
||||
fn dummy_rewarding_params() -> RewardingParams {
|
||||
RewardingParams {
|
||||
interval: IntervalRewardParams {
|
||||
reward_pool: Decimal::from_atomics(100_000_000_000_000u128, 0).unwrap(),
|
||||
staking_supply: Decimal::from_atomics(123_456_000_000_000u128, 0).unwrap(),
|
||||
staking_supply_scale_factor: Percent::hundred(),
|
||||
epoch_reward_budget: Decimal::from_ratio(100_000_000_000_000u128, 1234u32)
|
||||
* Decimal::percent(1),
|
||||
stake_saturation_point: Decimal::from_ratio(123_456_000_000_000u128, 313u32),
|
||||
sybil_resistance: Percent::from_percentage_value(23).unwrap(),
|
||||
active_set_work_factor: Decimal::from_atomics(10u32, 0).unwrap(),
|
||||
interval_pool_emission: Percent::from_percentage_value(1).unwrap(),
|
||||
},
|
||||
rewarded_set: RewardedSetParams {
|
||||
entry_gateways: 50,
|
||||
exit_gateways: 70,
|
||||
mixnodes: 120,
|
||||
standby: 20,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn determining_nodes_work() {
|
||||
let params = dummy_rewarding_params();
|
||||
// matched parameters
|
||||
let rewarded_set = RewardedSet {
|
||||
entry_gateways: (1..)
|
||||
.take(params.rewarded_set.entry_gateways as usize)
|
||||
.collect(),
|
||||
exit_gateways: (1000..)
|
||||
.take(params.rewarded_set.exit_gateways as usize)
|
||||
.collect(),
|
||||
layer1: (2000..)
|
||||
.take(params.rewarded_set.mixnodes as usize / 3)
|
||||
.collect(),
|
||||
layer2: (3000..)
|
||||
.take(params.rewarded_set.mixnodes as usize / 3)
|
||||
.collect(),
|
||||
layer3: (4000..)
|
||||
.take(params.rewarded_set.mixnodes as usize / 3)
|
||||
.collect(),
|
||||
standby: (5000..)
|
||||
.take(params.rewarded_set.standby as usize)
|
||||
.collect(),
|
||||
};
|
||||
|
||||
let work = determine_per_node_work(&rewarded_set, params);
|
||||
assert_eq!(work.active, params.active_node_work());
|
||||
assert_eq!(work.standby, params.standby_node_work());
|
||||
|
||||
// updated
|
||||
// here we're interested in the fact that the calculation does not panic, i.e. total work <= 1
|
||||
let params = dummy_rewarding_params();
|
||||
let rewarded_set = RewardedSet {
|
||||
entry_gateways: (1..).take(250).collect(),
|
||||
exit_gateways: (1000..).take(100).collect(),
|
||||
layer1: (2000..).take(10).collect(),
|
||||
layer2: (3000..).take(10).collect(),
|
||||
layer3: (4000..).take(10).collect(),
|
||||
standby: (5000..).take(5).collect(),
|
||||
};
|
||||
|
||||
let work = determine_per_node_work(&rewarded_set, params);
|
||||
assert_ne!(work.active, params.active_node_work());
|
||||
assert_ne!(work.standby, params.standby_node_work());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user