Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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 {
Expand Down
14 changes: 13 additions & 1 deletion common/cosmwasm-smart-contracts/mixnet-contract/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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> {
Expand All @@ -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]
Expand Down
230 changes: 211 additions & 19 deletions nym-api/src/epoch_operations/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>>>>,
Expand All @@ -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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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());
}
}
Loading