From 875502288c34a2af99da17b3f1293ea17eef59b4 Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 18 Nov 2024 15:07:26 +0100 Subject: [PATCH 01/20] Add compute rewards v1 --- Cargo.lock | 30 + .../get_node_providers_monthly_xdr_rewards.rs | 4 +- rs/registry/node_provider_rewards/Cargo.toml | 13 +- rs/registry/node_provider_rewards/src/lib.rs | 163 +---- .../src/{logs.rs => v0_logs.rs} | 0 .../node_provider_rewards/src/v0_rewards.rs | 159 ++++ .../node_provider_rewards/src/v1_logs.rs | 273 +++++++ .../node_provider_rewards/src/v1_rewards.rs | 692 ++++++++++++++++++ .../node_provider_rewards/src/v1_types.rs | 114 +++ 9 files changed, 1287 insertions(+), 161 deletions(-) rename rs/registry/node_provider_rewards/src/{logs.rs => v0_logs.rs} (100%) create mode 100644 rs/registry/node_provider_rewards/src/v0_rewards.rs create mode 100644 rs/registry/node_provider_rewards/src/v1_logs.rs create mode 100644 rs/registry/node_provider_rewards/src/v1_rewards.rs create mode 100644 rs/registry/node_provider_rewards/src/v1_types.rs diff --git a/Cargo.lock b/Cargo.lock index b0cb3551605..98a50aa6d5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,6 +275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if 1.0.0", + "const-random", "getrandom", "once_cell", "version_check", @@ -2551,6 +2552,26 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -11148,8 +11169,17 @@ dependencies = [ name = "ic-registry-node-provider-rewards" version = "0.9.0" dependencies = [ + "ahash 0.8.11", + "candid", "ic-base-types", + "ic-management-canister-types", "ic-protobuf", + "itertools 0.12.1", + "lazy_static", + "num-traits", + "rust_decimal", + "rust_decimal_macros", + "serde", ] [[package]] diff --git a/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs b/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs index 7c099c70621..2de971c70fe 100644 --- a/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs +++ b/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs @@ -10,7 +10,7 @@ use ic_protobuf::registry::{ use ic_registry_keys::{ DATA_CENTER_KEY_PREFIX, NODE_OPERATOR_RECORD_KEY_PREFIX, NODE_REWARDS_TABLE_KEY, }; -use ic_registry_node_provider_rewards::calculate_rewards_v0; +use ic_registry_node_provider_rewards::v0_rewards::calculate_rewards; use prost::Message; use std::collections::BTreeMap; @@ -37,7 +37,7 @@ impl Registry { let data_centers = get_key_family_iter::(self, DATA_CENTER_KEY_PREFIX) .collect::>(); - let reward_values = calculate_rewards_v0(&rewards_table, &node_operators, &data_centers)?; + let reward_values = calculate_rewards(&rewards_table, &node_operators, &data_centers)?; rewards.rewards = reward_values .rewards_per_node_provider diff --git a/rs/registry/node_provider_rewards/Cargo.toml b/rs/registry/node_provider_rewards/Cargo.toml index c5057697970..81242876b4e 100644 --- a/rs/registry/node_provider_rewards/Cargo.toml +++ b/rs/registry/node_provider_rewards/Cargo.toml @@ -7,5 +7,16 @@ documentation.workspace = true edition.workspace = true [dependencies] -ic-base-types = { path = "../../types/base_types/" } +ic-base-types = { path = "../../types/base_types" } +ahash = { version = "0.8.11", default-features = false, features = [ + "compile-time-rng", +] } ic-protobuf = { path = "../../protobuf" } +itertools = { workspace = true } +lazy_static = { workspace = true } +num-traits = { workspace = true } +rust_decimal = "1.36.0" +rust_decimal_macros = "1.36.0" +ic-management-canister-types = { path = "../../types/management_canister_types" } +serde = { workspace = true } +candid = { workspace = true } diff --git a/rs/registry/node_provider_rewards/src/lib.rs b/rs/registry/node_provider_rewards/src/lib.rs index e632de2984b..78b0ac08e5a 100644 --- a/rs/registry/node_provider_rewards/src/lib.rs +++ b/rs/registry/node_provider_rewards/src/lib.rs @@ -1,160 +1,7 @@ -use ic_base_types::PrincipalId; -use ic_protobuf::registry::{ - dc::v1::DataCenterRecord, - node_operator::v1::NodeOperatorRecord, - node_rewards::v2::{NodeRewardRate, NodeRewardsTable}, -}; -use logs::{LogEntry, RewardsPerNodeProviderLog}; -use std::collections::{BTreeMap, HashMap}; -pub mod logs; +pub mod v1_logs; +pub mod v1_rewards; +pub mod v1_types; -pub struct RewardsPerNodeProvider { - pub rewards_per_node_provider: BTreeMap, - pub computation_log: BTreeMap, -} +pub mod v0_logs; -pub fn calculate_rewards_v0( - rewards_table: &NodeRewardsTable, - node_operators: &[(String, NodeOperatorRecord)], - data_centers: &BTreeMap, -) -> Result { - // The reward coefficients for the NP, at the moment used only for type3 nodes, as a measure for stimulating decentralization. - // It is kept outside of the reward calculation loop in order to reduce node rewards for NPs with multiple DCs. - // We want to have as many independent NPs as possible for the given reward budget. - let mut np_coefficients: HashMap = HashMap::new(); - - let mut rewards = BTreeMap::new(); - let mut computation_log = BTreeMap::new(); - - for (key_string, node_operator) in node_operators.iter() { - let node_operator_id = PrincipalId::try_from(&node_operator.node_operator_principal_id) - .map_err(|e| { - format!( - "Node Operator key '{:?}' cannot be parsed as a PrincipalId: '{}'", - key_string, e - ) - })?; - - let node_provider_id = PrincipalId::try_from(&node_operator.node_provider_principal_id) - .map_err(|e| { - format!( - "Node Operator with key '{}' has a node_provider_principal_id \ - that cannot be parsed as a PrincipalId: '{}'", - node_operator_id, e - ) - })?; - - let dc = data_centers.get(&node_operator.dc_id).ok_or_else(|| { - format!( - "Node Operator with key '{}' has data center ID '{}' \ - not found in the Registry", - node_operator_id, node_operator.dc_id - ) - })?; - let region = &dc.region; - - let np_rewards = rewards.entry(node_provider_id).or_default(); - let np_log = computation_log - .entry(node_provider_id) - .or_insert(RewardsPerNodeProviderLog::new(node_provider_id)); - - for (node_type, node_count) in node_operator.rewardable_nodes.iter() { - let rate = match rewards_table.get_rate(region, node_type) { - Some(rate) => rate, - None => { - np_log.add_entry(LogEntry::RateNotFoundInRewardTable { - region: region.clone(), - node_type: node_type.clone(), - node_operator_id, - }); - - NodeRewardRate { - xdr_permyriad_per_node_per_month: 1, - reward_coefficient_percent: Some(100), - } - } - }; - - let dc_reward = match &node_type { - t if t.starts_with("type3") => { - // For type3 nodes, the rewards are progressively reduced for each additional node owned by a NP. - // This helps to improve network decentralization. The first node gets the full reward. - // After the first node, the rewards are progressively reduced by multiplying them with reward_coefficient_percent. - // For the n-th node, the reward is: - // reward(n) = reward(n-1) * reward_coefficient_percent ^ (n-1) - // - // A note around the type3 rewards and iter() over self.store - // - // One known issue with this implementation is that in some edge cases it could lead to - // unexpected results. The outer loop iterates over the node operator records sorted - // lexicographically, instead of the order in which the records were added to the registry, - // or instead of the order in which NP/NO adds nodes to the network. This means that all - // reduction factors for the node operator A are applied prior to all reduction factors for - // the node operator B, independently from the order in which the node operator records, - // nodes, or the rewardable nodes were added to the registry. - // For instance, say a Node Provider adds a Node Operator B in region 1 with higher reward - // coefficient so higher average rewards, and then A in region 2 with lower reward - // coefficient so lower average rewards. When the rewards are calculated, the rewards for - // Node Operator A are calculated before the rewards for B (due to the lexicographical - // order), and the final rewards will be lower than they would be calculated first for B and - // then for A, as expected based on the insert order. - - let reward_base = rate.xdr_permyriad_per_node_per_month as f64; - - // To de-stimulate the same NP having too many nodes in the same country, the node rewards - // is reduced for each node the NP has in the given country. - // Join the NP PrincipalId + DC Continent + DC Country, and use that as the key for the - // reduction coefficients. - let np_coefficients_key = format!( - "{}:{}", - node_provider_id, - region - .splitn(3, ',') - .take(2) - .collect::>() - .join(":") - ); - - let mut np_coeff = *np_coefficients.get(&np_coefficients_key).unwrap_or(&1.0); - - // Default reward_coefficient_percent is set to 80%, which is used as a fallback only in the - // unlikely case that the type3 entry in the reward table: - // a) has xdr_permyriad_per_node_per_month entry set for this region, but - // b) does NOT have the reward_coefficient_percent value set - let dc_reward_coefficient_percent = - rate.reward_coefficient_percent.unwrap_or(80) as f64 / 100.0; - - let mut dc_reward = 0; - for i in 0..*node_count { - let node_reward = (reward_base * np_coeff) as u64; - np_log.add_entry(LogEntry::NodeRewards { - node_type: node_type.clone(), - node_idx: i, - dc_id: node_operator.dc_id.clone(), - rewardable_count: *node_count, - rewards_xdr_permyriad: node_reward, - }); - dc_reward += node_reward; - np_coeff *= dc_reward_coefficient_percent; - } - np_coefficients.insert(np_coefficients_key, np_coeff); - dc_reward - } - _ => *node_count as u64 * rate.xdr_permyriad_per_node_per_month, - }; - - np_log.add_entry(LogEntry::DCRewards { - dc_id: node_operator.dc_id.clone(), - node_type: node_type.clone(), - rewardable_count: *node_count, - rewards_xdr_permyriad: dc_reward, - }); - *np_rewards += dc_reward; - } - } - - Ok(RewardsPerNodeProvider { - rewards_per_node_provider: rewards, - computation_log, - }) -} +pub mod v0_rewards; diff --git a/rs/registry/node_provider_rewards/src/logs.rs b/rs/registry/node_provider_rewards/src/v0_logs.rs similarity index 100% rename from rs/registry/node_provider_rewards/src/logs.rs rename to rs/registry/node_provider_rewards/src/v0_logs.rs diff --git a/rs/registry/node_provider_rewards/src/v0_rewards.rs b/rs/registry/node_provider_rewards/src/v0_rewards.rs new file mode 100644 index 00000000000..f2cfc0fb8cb --- /dev/null +++ b/rs/registry/node_provider_rewards/src/v0_rewards.rs @@ -0,0 +1,159 @@ +use crate::v0_logs::{LogEntry, RewardsPerNodeProviderLog}; +use ic_base_types::PrincipalId; +use ic_protobuf::registry::{ + dc::v1::DataCenterRecord, + node_operator::v1::NodeOperatorRecord, + node_rewards::v2::{NodeRewardRate, NodeRewardsTable}, +}; +use std::collections::{BTreeMap, HashMap}; + +pub struct RewardsPerNodeProvider { + pub rewards_per_node_provider: BTreeMap, + pub computation_log: BTreeMap, +} + +pub fn calculate_rewards( + rewards_table: &NodeRewardsTable, + node_operators: &[(String, NodeOperatorRecord)], + data_centers: &BTreeMap, +) -> Result { + // The reward coefficients for the NP, at the moment used only for type3 nodes, as a measure for stimulating decentralization. + // It is kept outside of the reward calculation loop in order to reduce node rewards for NPs with multiple DCs. + // We want to have as many independent NPs as possible for the given reward budget. + let mut np_coefficients: HashMap = HashMap::new(); + + let mut rewards = BTreeMap::new(); + let mut computation_log = BTreeMap::new(); + + for (key_string, node_operator) in node_operators.iter() { + let node_operator_id = PrincipalId::try_from(&node_operator.node_operator_principal_id) + .map_err(|e| { + format!( + "Node Operator key '{:?}' cannot be parsed as a PrincipalId: '{}'", + key_string, e + ) + })?; + + let node_provider_id = PrincipalId::try_from(&node_operator.node_provider_principal_id) + .map_err(|e| { + format!( + "Node Operator with key '{}' has a node_provider_principal_id \ + that cannot be parsed as a PrincipalId: '{}'", + node_operator_id, e + ) + })?; + + let dc = data_centers.get(&node_operator.dc_id).ok_or_else(|| { + format!( + "Node Operator with key '{}' has data center ID '{}' \ + not found in the Registry", + node_operator_id, node_operator.dc_id + ) + })?; + let region = &dc.region; + + let np_rewards = rewards.entry(node_provider_id).or_default(); + let np_log = computation_log + .entry(node_provider_id) + .or_insert(RewardsPerNodeProviderLog::new(node_provider_id)); + + for (node_type, node_count) in node_operator.rewardable_nodes.iter() { + let rate = match rewards_table.get_rate(region, node_type) { + Some(rate) => rate, + None => { + np_log.add_entry(LogEntry::RateNotFoundInRewardTable { + region: region.clone(), + node_type: node_type.clone(), + node_operator_id, + }); + + NodeRewardRate { + xdr_permyriad_per_node_per_month: 1, + reward_coefficient_percent: Some(100), + } + } + }; + + let dc_reward = match &node_type { + t if t.starts_with("type3") => { + // For type3 nodes, the rewards are progressively reduced for each additional node owned by a NP. + // This helps to improve network decentralization. The first node gets the full reward. + // After the first node, the rewards are progressively reduced by multiplying them with reward_coefficient_percent. + // For the n-th node, the reward is: + // reward(n) = reward(n-1) * reward_coefficient_percent ^ (n-1) + // + // A note around the type3 rewards and iter() over self.store + // + // One known issue with this implementation is that in some edge cases it could lead to + // unexpected results. The outer loop iterates over the node operator records sorted + // lexicographically, instead of the order in which the records were added to the registry, + // or instead of the order in which NP/NO adds nodes to the network. This means that all + // reduction factors for the node operator A are applied prior to all reduction factors for + // the node operator B, independently from the order in which the node operator records, + // nodes, or the rewardable nodes were added to the registry. + // For instance, say a Node Provider adds a Node Operator B in region 1 with higher reward + // coefficient so higher average rewards, and then A in region 2 with lower reward + // coefficient so lower average rewards. When the rewards are calculated, the rewards for + // Node Operator A are calculated before the rewards for B (due to the lexicographical + // order), and the final rewards will be lower than they would be calculated first for B and + // then for A, as expected based on the insert order. + + let reward_base = rate.xdr_permyriad_per_node_per_month as f64; + + // To de-stimulate the same NP having too many nodes in the same country, the node rewards + // is reduced for each node the NP has in the given country. + // Join the NP PrincipalId + DC Continent + DC Country, and use that as the key for the + // reduction coefficients. + let np_coefficients_key = format!( + "{}:{}", + node_provider_id, + region + .splitn(3, ',') + .take(2) + .collect::>() + .join(":") + ); + + let mut np_coeff = *np_coefficients.get(&np_coefficients_key).unwrap_or(&1.0); + + // Default reward_coefficient_percent is set to 80%, which is used as a fallback only in the + // unlikely case that the type3 entry in the reward table: + // a) has xdr_permyriad_per_node_per_month entry set for this region, but + // b) does NOT have the reward_coefficient_percent value set + let dc_reward_coefficient_percent = + rate.reward_coefficient_percent.unwrap_or(80) as f64 / 100.0; + + let mut dc_reward = 0; + for i in 0..*node_count { + let node_reward = (reward_base * np_coeff) as u64; + np_log.add_entry(LogEntry::NodeRewards { + node_type: node_type.clone(), + node_idx: i, + dc_id: node_operator.dc_id.clone(), + rewardable_count: *node_count, + rewards_xdr_permyriad: node_reward, + }); + dc_reward += node_reward; + np_coeff *= dc_reward_coefficient_percent; + } + np_coefficients.insert(np_coefficients_key, np_coeff); + dc_reward + } + _ => *node_count as u64 * rate.xdr_permyriad_per_node_per_month, + }; + + np_log.add_entry(LogEntry::DCRewards { + dc_id: node_operator.dc_id.clone(), + node_type: node_type.clone(), + rewardable_count: *node_count, + rewards_xdr_permyriad: dc_reward, + }); + *np_rewards += dc_reward; + } + } + + Ok(RewardsPerNodeProvider { + rewards_per_node_provider: rewards, + computation_log, + }) +} diff --git a/rs/registry/node_provider_rewards/src/v1_logs.rs b/rs/registry/node_provider_rewards/src/v1_logs.rs new file mode 100644 index 00000000000..208daa6c456 --- /dev/null +++ b/rs/registry/node_provider_rewards/src/v1_logs.rs @@ -0,0 +1,273 @@ +use ic_base_types::PrincipalId; +use itertools::Itertools; +use rust_decimal::{prelude::Zero, Decimal}; +use std::fmt; + +#[derive(Clone)] +pub enum Operation { + Sum(Vec), + Avg(Vec), + Subtract(Decimal, Decimal), + Multiply(Decimal, Decimal), + Divide(Decimal, Decimal), + Set(Decimal), + SumOps(Vec), +} + +impl Operation { + fn sum(operators: &[Decimal]) -> Decimal { + operators.iter().fold(Decimal::zero(), |acc, val| acc + val) + } + + fn format_values(items: &[T], prefix: &str) -> String { + if items.is_empty() { + "0".to_string() + } else { + format!( + "{}({})", + prefix, + items.iter().map(|item| format!("{}", item)).join(","), + ) + } + } + + fn execute(&self) -> Decimal { + match self { + Operation::Sum(operators) => Self::sum(operators), + Operation::Avg(operators) => { + Self::sum(operators) / Decimal::from(operators.len().max(1)) + } + Operation::Subtract(o1, o2) => o1 - o2, + Operation::Divide(o1, o2) => o1 / o2, + Operation::Multiply(o1, o2) => o1 * o2, + Operation::Set(o1) => *o1, + Operation::SumOps(operations) => Self::sum( + &operations + .iter() + .map(|operation| operation.execute()) + .collect_vec(), + ), + } + } +} + +impl fmt::Display for Operation { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let (symbol, o1, o2) = match self { + Operation::Sum(values) => { + return write!(f, "{}", Operation::format_values(values, "sum")) + } + Operation::SumOps(operations) => { + return write!(f, "{}", Operation::format_values(operations, "sum")) + } + Operation::Avg(values) => { + return write!(f, "{}", Operation::format_values(values, "avg")) + } + Operation::Subtract(o1, o2) => ("-", o1, o2), + Operation::Divide(o1, o2) => ("/", o1, o2), + Operation::Multiply(o1, o2) => ("*", o1, o2), + Operation::Set(o1) => return write!(f, "set {}", o1), + }; + write!(f, "{} {} {}", o1.round_dp(4), symbol, o2.round_dp(4)) + } +} + +pub enum LogEntry { + RewardsForNodeProvider(PrincipalId, u32), + RewardMultiplierForNode(PrincipalId, Decimal), + RewardsXDRTotal(Decimal), + Execute { + reason: String, + operation: Operation, + result: Decimal, + }, + PerformanceBasedRewardables { + node_type: String, + region: String, + count: usize, + assigned_multipliers: Vec, + unassigned_multipliers: Vec, + }, + RateNotFoundInRewardTable { + node_type: String, + region: String, + }, + RewardTableEntry { + node_type: String, + region: String, + coeff: Decimal, + base_rewards: Decimal, + }, + AvgType3Rewards { + region: String, + rewards_avg: Decimal, + coefficients_avg: Decimal, + region_rewards_avg: Decimal, + }, + UnassignedMultiplier(Decimal), + NodeCountRewardables { + node_type: String, + region: String, + count: usize, + }, +} + +impl fmt::Display for LogEntry { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LogEntry::Execute { + reason, + operation, + result, + } => { + write!( + f, + "ExecuteOperation | reason={}, operation={}, result={}", + reason, + operation, + result.round_dp(2) + ) + } + LogEntry::RewardsForNodeProvider(principal, node_count) => { + write!( + f, + "Node Provider: {} rewardable nodes in period: {}", + principal, node_count + ) + } + LogEntry::RewardMultiplierForNode(principal, multiplier) => { + write!( + f, + "Rewards Multiplier for node: {} is {}", + principal, + multiplier.round_dp(2) + ) + } + LogEntry::RewardsXDRTotal(rewards_xdr_total) => { + write!( + f, + "Total rewards XDR permyriad: {}", + rewards_xdr_total.round_dp(2) + ) + } + LogEntry::RateNotFoundInRewardTable { node_type, region } => { + write!( + f, + "RateNotFoundInRewardTable | node_type={}, region={}", + node_type, region + ) + } + LogEntry::RewardTableEntry { + node_type, + region, + coeff, + base_rewards, + } => { + write!( + f, + "RewardTableEntry | node_type={}, region={}, coeff={}, base_rewards={}", + node_type, region, coeff, base_rewards + ) + } + LogEntry::PerformanceBasedRewardables { + node_type, + region, + count, + assigned_multipliers: assigned_multiplier, + unassigned_multipliers: unassigned_multiplier, + } => { + write!( + f, + "Region {} with type: {} | Rewardable Nodes: {} Assigned Multipliers: {:?} Unassigned Multipliers: {:?}", + region, + node_type, + count, + assigned_multiplier.iter().map(|dec| dec.round_dp(2)).collect_vec(), + unassigned_multiplier.iter().map(|dec| dec.round_dp(2)).collect_vec() + ) + } + LogEntry::AvgType3Rewards { + region, + rewards_avg, + coefficients_avg, + region_rewards_avg, + } => { + write!( + f, + "Avg. rewards for nodes with type: type3* in region: {} is {}\nRegion rewards average: {}\nReduction coefficient average:{}", + region, + rewards_avg.round_dp(2), + region_rewards_avg, + coefficients_avg + ) + } + LogEntry::UnassignedMultiplier(unassigned_multiplier) => { + write!( + f, + "Unassigned Nodes Multiplier: {}", + unassigned_multiplier.round_dp(2) + ) + } + LogEntry::NodeCountRewardables { + node_type, + region, + count, + } => { + write!( + f, + "Region {} with type: {} | Rewardable Nodes: {} Rewarded independently of their performance", + region, node_type, count + ) + } + } + } +} + +#[derive(Copy, Clone)] +pub enum LogLevel { + Info, + Debug, +} + +impl fmt::Display for LogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + LogLevel::Info => write!(f, "INFO"), + LogLevel::Debug => write!(f, "DEBUG"), + } + } +} + +#[derive(Default)] +pub struct RewardsLog { + entries: Vec<(LogLevel, LogEntry)>, +} + +impl RewardsLog { + pub fn add_entry(&mut self, entry: LogEntry) { + self.entries.push((LogLevel::Info, entry)); + } + + pub fn execute(&mut self, reason: &str, operation: Operation) -> Decimal { + let result = operation.execute(); + let entry = LogEntry::Execute { + reason: reason.to_string(), + operation, + result, + }; + self.entries.push((LogLevel::Debug, entry)); + result + } + + pub fn get_log(&self, level: LogLevel) -> Vec { + self.entries + .iter() + .filter_map( + move |(entry_log_level, entry)| match (level, entry_log_level) { + (LogLevel::Info, LogLevel::Debug) => None, + _ => Some(format!("{}: {} ", level, entry)), + }, + ) + .collect_vec() + } +} diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs new file mode 100644 index 00000000000..6d60b18a373 --- /dev/null +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -0,0 +1,692 @@ +use ic_base_types::PrincipalId; +use ic_protobuf::registry::node_rewards::v2::{NodeRewardRate, NodeRewardsTable}; +use itertools::Itertools; +use lazy_static::lazy_static; +use num_traits::ToPrimitive; + +use crate::{ + v1_logs::{LogEntry, Operation, RewardsLog}, + v1_types::{ + AHashMap, DailyNodeMetrics, MultiplierStats, NodeMultiplierStats, RegionNodeTypeCategory, + RewardableNode, RewardablesWithNodesMetrics, Rewards, RewardsPerNodeProvider, + }, +}; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use std::{ + mem, + sync::{Arc, RwLock}, +}; + +const FULL_REWARDS_MACHINES_LIMIT: u32 = 4; +const MIN_FAILURE_RATE: Decimal = dec!(0.1); +const MAX_FAILURE_RATE: Decimal = dec!(0.6); + +const RF: &str = "Linear Reduction factor"; + +lazy_static! { + static ref LOGGER: Arc> = Arc::new(RwLock::new(RewardsLog::default())); +} + +fn logger() -> std::sync::RwLockWriteGuard<'static, RewardsLog> { + LOGGER.write().unwrap() +} + +/// Calculates the rewards reduction based on the failure rate. +/// +/// if `failure_rate` is: +/// - Below the `MIN_FAILURE_RATE`, no reduction in rewards applied. +/// - Above the `MAX_FAILURE_RATE`, maximum reduction in rewards applied. +/// - Within the defined range (`MIN_FAILURE_RATE` to `MAX_FAILURE_RATE`), +/// the function calculates the reduction from the linear reduction function. +fn rewards_reduction_percent(failure_rate: &Decimal) -> Decimal { + if failure_rate < &MIN_FAILURE_RATE { + logger().execute( + &format!( + "No Reduction applied because {} is less than {} failure rate.\n{}", + failure_rate.round_dp(4), + MIN_FAILURE_RATE, + RF + ), + Operation::Set(dec!(0)), + ) + } else if failure_rate > &MAX_FAILURE_RATE { + logger().execute( + &format!( + "Max reduction applied because {} is over {} failure rate.\n{}", + failure_rate.round_dp(4), + MAX_FAILURE_RATE, + RF + ), + Operation::Set(dec!(0.8)), + ) + } else { + let y_change = logger().execute( + "Linear Reduction Y change", + Operation::Subtract(*failure_rate, MIN_FAILURE_RATE), + ); + let x_change = logger().execute( + "Linear Reduction X change", + Operation::Subtract(MAX_FAILURE_RATE, MIN_FAILURE_RATE), + ); + + let m = logger().execute("Compute m", Operation::Divide(y_change, x_change)); + + logger().execute(RF, Operation::Multiply(m, dec!(0.8))) + } +} + +/// Assigned nodes multiplier +/// +/// Computes the rewards multiplier for a single assigned node based on the overall failure rate in the period. +/// +/// 1. The function iterates through each day's metrics, summing up the `daily_failed` and `daily_total` blocks across all days. +/// 2. The `overall_failure_rate` for the period is calculated by dividing the `overall_failed` blocks by the `overall_total` blocks. +/// 3. The `rewards_reduction` function is applied to `overall_failure_rate`. +/// 3. Finally, the rewards multiplier to be distributed to the node is computed. +pub fn assigned_nodes_multiplier( + daily_metrics: &[DailyNodeMetrics], + total_days: u64, +) -> (Decimal, MultiplierStats) { + let total_days = Decimal::from(total_days); + + let days_assigned = logger().execute( + "Assigned Days In Period", + Operation::Set(Decimal::from(daily_metrics.len())), + ); + let days_unassigned = logger().execute( + "Unassigned Days In Period", + Operation::Subtract(total_days, days_assigned), + ); + + let daily_failed = daily_metrics + .iter() + .map(|metrics| metrics.num_blocks_failed.into()) + .collect_vec(); + let daily_proposed = daily_metrics + .iter() + .map(|metrics| metrics.num_blocks_proposed.into()) + .collect_vec(); + + let overall_failed = logger().execute( + "Computing Total Failed Blocks", + Operation::Sum(daily_failed), + ); + let overall_proposed = logger().execute( + "Computing Total Proposed Blocks", + Operation::Sum(daily_proposed), + ); + let overall_total = logger().execute( + "Computing Total Blocks", + Operation::Sum(vec![overall_failed, overall_proposed]), + ); + let overall_failure_rate = logger().execute( + "Computing Total Failure Rate", + if overall_total > dec!(0) { + Operation::Divide(overall_failed, overall_total) + } else { + Operation::Set(dec!(0)) + }, + ); + + let rewards_reduction = rewards_reduction_percent(&overall_failure_rate); + let rewards_multiplier_assigned = logger().execute( + "Reward Multiplier Assigned Days", + Operation::Subtract(dec!(1), rewards_reduction), + ); + + // On days when the node is not assigned to a subnet, it will receive the same `Reward Multiplier` as computed for the days it was assigned. + let rewards_multiplier_unassigned = logger().execute( + "Reward Multiplier Unassigned Days", + Operation::Set(rewards_multiplier_assigned), + ); + let assigned_days_factor = logger().execute( + "Assigned Days Factor", + Operation::Multiply(days_assigned, rewards_multiplier_assigned), + ); + let unassigned_days_factor = logger().execute( + "Unassigned Days Factor (currently equal to Assigned Days Factor)", + Operation::Multiply(days_unassigned, rewards_multiplier_unassigned), + ); + let rewards_multiplier = logger().execute( + "Average reward multiplier", + Operation::Divide(assigned_days_factor + unassigned_days_factor, total_days), + ); + + let rewards_multiplier_stats = MultiplierStats { + days_assigned: days_assigned.to_u64().unwrap(), + days_unassigned: days_unassigned.to_u64().unwrap(), + rewards_reduction: rewards_reduction.to_f64().unwrap(), + blocks_failed: overall_failed.to_u64().unwrap(), + blocks_proposed: overall_proposed.to_u64().unwrap(), + blocks_total: overall_total.to_u64().unwrap(), + failure_rate: overall_failure_rate.to_f64().unwrap(), + }; + + (rewards_multiplier, rewards_multiplier_stats) +} + +fn region_type3_key(region: String) -> RegionNodeTypeCategory { + // The rewards table contains entries of this form DC Continent + DC Country + DC State/City. + // The grouping for type3* nodes will be on DC Continent + DC Country level. This group is used for computing + // the reduction coefficient and base reward for the group. + + let region_key = region + .splitn(3, ',') + .take(2) + .collect::>() + .join(":"); + (region_key, "type3*".to_string()) +} + +fn base_rewards_region_nodetype( + rewardable_nodes: &AHashMap, + rewards_table: &NodeRewardsTable, +) -> AHashMap { + let mut type3_coefficients_rewards: AHashMap< + RegionNodeTypeCategory, + (Vec, Vec), + > = AHashMap::default(); + let mut region_nodetype_rewards: AHashMap = + AHashMap::default(); + + for ((region, node_type), node_count) in rewardable_nodes { + let rate = match rewards_table.get_rate(region, node_type) { + Some(rate) => rate, + None => { + logger().add_entry(LogEntry::RateNotFoundInRewardTable { + node_type: node_type.clone(), + region: region.clone(), + }); + + NodeRewardRate { + xdr_permyriad_per_node_per_month: 1, + reward_coefficient_percent: Some(100), + } + } + }; + let base_rewards = Decimal::from(rate.xdr_permyriad_per_node_per_month); + let mut coeff = dec!(1); + + if node_type.starts_with("type3") && *node_count > 0 { + // For nodes which are type3* the base rewards for the single node is computed as the average of base rewards + // on DC Country level. Moreover, to de-stimulate the same NP having too many nodes in the same country, + // the node rewards is reduced for each node the NP has in the given country. The reduction coefficient is + // computed as the average of reduction coefficients on DC Country level. + + coeff = Decimal::from(rate.reward_coefficient_percent.unwrap_or(80)) / dec!(100); + let coefficients = vec![coeff; *node_count as usize]; + let base_rewards = vec![base_rewards; *node_count as usize]; + let region_key = region_type3_key(region.clone()); + + type3_coefficients_rewards + .entry(region_key) + .and_modify(|(entry_coefficients, entry_rewards)| { + entry_coefficients.extend(&coefficients); + entry_rewards.extend(&base_rewards); + }) + .or_insert((coefficients, base_rewards)); + } else { + // For `rewardable_nodes` which are not type3* the base rewards for the sigle node is the entry + // in the rewards table for the specific region (DC Continent + DC Country + DC State/City) and node type. + + region_nodetype_rewards.insert((region.clone(), node_type.clone()), base_rewards); + } + + logger().add_entry(LogEntry::RewardTableEntry { + node_type: node_type.clone(), + region: region.clone(), + coeff, + base_rewards, + }); + } + + // Computes node rewards for type3* nodes in all regions and add it to region_nodetype_rewards + for (key, (coefficients, rewards)) in type3_coefficients_rewards { + let rewards_len = rewards.len(); + let mut running_coefficient = dec!(1); + let mut region_rewards = Vec::new(); + + let coefficients_avg = logger().execute("Coefficients avg.", Operation::Avg(coefficients)); + let rewards_avg = logger().execute("Rewards avg.", Operation::Avg(rewards)); + for _ in 0..rewards_len { + region_rewards.push(Operation::Multiply(rewards_avg, running_coefficient)); + running_coefficient *= coefficients_avg; + } + let region_rewards = logger().execute( + "Total rewards after coefficient reduction", + Operation::SumOps(region_rewards), + ); + let region_rewards_avg = logger().execute( + "Rewards average after coefficient reduction", + Operation::Divide(region_rewards, Decimal::from(rewards_len)), + ); + + logger().add_entry(LogEntry::AvgType3Rewards { + region: key.0.clone(), + rewards_avg, + coefficients_avg, + region_rewards_avg, + }); + + region_nodetype_rewards.insert(key, region_rewards_avg); + } + + region_nodetype_rewards +} + +fn node_provider_rewards( + assigned_multipliers: &AHashMap>, + rewardable_nodes: &AHashMap, + rewards_table: &NodeRewardsTable, +) -> Rewards { + let mut rewards_xdr_total = Vec::new(); + let mut rewards_xdr_no_penalty_total = Vec::new(); + let rewardable_nodes_count: u32 = rewardable_nodes.values().sum(); + + let region_nodetype_rewards: AHashMap = + base_rewards_region_nodetype(rewardable_nodes, rewards_table); + + // Computes the rewards multiplier for unassigned nodes as the average of the multipliers of the assigned nodes. + let assigned_multipliers_v = assigned_multipliers + .values() + .flatten() + .cloned() + .collect_vec(); + let unassigned_multiplier = logger().execute( + "Unassigned Nodes Multiplier", + Operation::Avg(assigned_multipliers_v), + ); + logger().add_entry(LogEntry::UnassignedMultiplier(unassigned_multiplier)); + + for ((region, node_type), node_count) in rewardable_nodes { + let xdr_permyriad = if node_type.starts_with("type3") { + let region_key = region_type3_key(region.clone()); + region_nodetype_rewards + .get(®ion_key) + .expect("Type3 rewards already filled") + } else { + region_nodetype_rewards + .get(&(region.clone(), node_type.clone())) + .expect("Rewards already filled") + }; + let rewards_xdr_no_penalty = + Operation::Multiply(*xdr_permyriad, Decimal::from(*node_count)); + rewards_xdr_no_penalty_total.push(rewards_xdr_no_penalty.clone()); + + // Node Providers with less than 4 machines are rewarded fully, independently of their performance + if rewardable_nodes_count < FULL_REWARDS_MACHINES_LIMIT { + logger().add_entry(LogEntry::NodeCountRewardables { + node_type: node_type.clone(), + region: region.clone(), + count: *node_count as usize, + }); + + rewards_xdr_total.push(rewards_xdr_no_penalty); + } else { + let mut rewards_multipliers = assigned_multipliers + .get(&(region.clone(), node_type.clone())) + .cloned() + .unwrap_or_default(); + let assigned_len = rewards_multipliers.len(); + + rewards_multipliers.resize(*node_count as usize, unassigned_multiplier); + + logger().add_entry(LogEntry::PerformanceBasedRewardables { + node_type: node_type.clone(), + region: region.clone(), + count: *node_count as usize, + assigned_multipliers: rewards_multipliers[..assigned_len].to_vec(), + unassigned_multipliers: rewards_multipliers[assigned_len..].to_vec(), + }); + + for multiplier in rewards_multipliers { + rewards_xdr_total.push(Operation::Multiply(*xdr_permyriad, multiplier)); + } + } + } + + let rewards_xdr_total = logger().execute( + "Compute total permyriad XDR", + Operation::SumOps(rewards_xdr_total), + ); + let rewards_xdr_no_reduction_total = logger().execute( + "Compute total permyriad XDR no performance penalty", + Operation::SumOps(rewards_xdr_no_penalty_total), + ); + logger().add_entry(LogEntry::RewardsXDRTotal(rewards_xdr_total)); + + Rewards { + xdr_permyriad: rewards_xdr_total.to_u64().unwrap(), + xdr_permyriad_no_reduction: rewards_xdr_no_reduction_total.to_u64().unwrap(), + } +} + +fn node_providers_rewardables( + nodes: &[RewardableNode], +) -> AHashMap { + let mut node_provider_rewardables: AHashMap = + AHashMap::default(); + + nodes.iter().for_each(|node| { + let (rewardable_nodes, assigned_metrics) = node_provider_rewardables + .entry(node.node_provider_id) + .or_default(); + + let nodes_count = rewardable_nodes + .entry((node.region.clone(), node.node_type.clone())) + .or_default(); + *nodes_count += 1; + + if let Some(daily_metrics) = &node.node_metrics { + assigned_metrics.insert(node.clone(), daily_metrics.clone()); + } + }); + + node_provider_rewardables +} + +pub fn calculate_rewards( + days_in_period: u64, + rewards_table: &NodeRewardsTable, + rewardable_nodes: &[RewardableNode], +) -> RewardsPerNodeProvider { + let mut rewards_per_node_provider = AHashMap::default(); + let mut rewards_log_per_node_provider = AHashMap::default(); + let node_provider_rewardables = node_providers_rewardables(rewardable_nodes); + + for (node_provider_id, (rewardable_nodes, assigned_nodes_metrics)) in node_provider_rewardables + { + let mut assigned_multipliers: AHashMap> = + AHashMap::default(); + let mut nodes_multiplier_stats: Vec = Vec::new(); + let total_rewardable_nodes: u32 = rewardable_nodes.values().sum(); + + logger().add_entry(LogEntry::RewardsForNodeProvider( + node_provider_id, + total_rewardable_nodes, + )); + + for (node, daily_metrics) in assigned_nodes_metrics { + let (multiplier, multiplier_stats) = + assigned_nodes_multiplier(&daily_metrics, days_in_period); + logger().add_entry(LogEntry::RewardMultiplierForNode(node.node_id, multiplier)); + nodes_multiplier_stats.push((node.node_id, multiplier_stats)); + assigned_multipliers + .entry((node.region.clone(), node.node_type.clone())) + .or_default() + .push(multiplier); + } + + let rewards = + node_provider_rewards(&assigned_multipliers, &rewardable_nodes, rewards_table); + let node_provider_log = mem::take(&mut *logger()); + + rewards_log_per_node_provider.insert(node_provider_id, node_provider_log); + rewards_per_node_provider.insert(node_provider_id, (rewards, nodes_multiplier_stats)); + } + + RewardsPerNodeProvider { + rewards_per_node_provider, + rewards_log_per_node_provider, + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use ic_protobuf::registry::node_rewards::v2::NodeRewardRates; + use itertools::Itertools; + + use super::*; + + #[derive(Clone)] + struct MockedMetrics { + days: u64, + proposed_blocks: u64, + failed_blocks: u64, + } + + impl MockedMetrics { + fn new(days: u64, proposed_blocks: u64, failed_blocks: u64) -> Self { + MockedMetrics { + days, + proposed_blocks, + failed_blocks, + } + } + } + + fn daily_mocked_metrics(metrics: Vec) -> Vec { + metrics + .into_iter() + .flat_map(|mocked_metrics: MockedMetrics| { + (0..mocked_metrics.days).map(move |_| DailyNodeMetrics { + num_blocks_proposed: mocked_metrics.proposed_blocks, + num_blocks_failed: mocked_metrics.failed_blocks, + }) + }) + .collect_vec() + } + + fn mocked_rewards_table() -> NodeRewardsTable { + let mut rates_outer: BTreeMap = BTreeMap::new(); + let mut rates_inner: BTreeMap = BTreeMap::new(); + let mut table: BTreeMap = BTreeMap::new(); + + let rate_outer = NodeRewardRate { + xdr_permyriad_per_node_per_month: 1000, + reward_coefficient_percent: Some(97), + }; + + let rate_inner = NodeRewardRate { + xdr_permyriad_per_node_per_month: 1500, + reward_coefficient_percent: Some(95), + }; + + rates_outer.insert("type0".to_string(), rate_outer); + rates_outer.insert("type1".to_string(), rate_outer); + rates_outer.insert("type3".to_string(), rate_outer); + + rates_inner.insert("type3.1".to_string(), rate_inner); + + table.insert("A,B,C".to_string(), NodeRewardRates { rates: rates_inner }); + table.insert("A,B".to_string(), NodeRewardRates { rates: rates_outer }); + + NodeRewardsTable { table } + } + + #[test] + fn test_rewards_percent() { + // Overall failed = 130 Overall total = 500 Failure rate = 0.26 + let daily_metrics: Vec = daily_mocked_metrics(vec![ + MockedMetrics::new(20, 6, 4), + MockedMetrics::new(25, 10, 2), + ]); + let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + assert_eq!(result, dec!(0.744)); + + // Overall failed = 45 Overall total = 450 Failure rate = 0.1 + // rewards_reduction = 0.0 + let daily_metrics: Vec = daily_mocked_metrics(vec![ + MockedMetrics::new(1, 400, 20), + MockedMetrics::new(1, 5, 25), // no penalty + ]); + let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + assert_eq!(result, dec!(1.0)); + + // Overall failed = 5 Overall total = 10 Failure rate = 0.5 + let daily_metrics: Vec = daily_mocked_metrics(vec![ + MockedMetrics::new(1, 5, 5), // no penalty + ]); + let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + assert_eq!(result, dec!(0.36)); + } + + #[test] + fn test_rewards_percent_max_reduction() { + let daily_metrics: Vec = daily_mocked_metrics(vec![ + MockedMetrics::new(10, 5, 95), // max failure rate + ]); + let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + assert_eq!(result, dec!(0.2)); + } + + #[test] + fn test_rewards_percent_min_reduction() { + let daily_metrics: Vec = daily_mocked_metrics(vec![ + MockedMetrics::new(10, 9, 1), // min failure rate + ]); + let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + assert_eq!(result, dec!(1.0)); + } + + #[test] + fn test_same_rewards_percent_if_gaps_no_penalty() { + let gap = MockedMetrics::new(1, 10, 0); + let daily_metrics_mid_gap: Vec = daily_mocked_metrics(vec![ + MockedMetrics::new(1, 6, 4), + gap.clone(), + MockedMetrics::new(1, 7, 3), + ]); + let daily_metrics_left_gap: Vec = daily_mocked_metrics(vec![ + gap.clone(), + MockedMetrics::new(1, 6, 4), + MockedMetrics::new(1, 7, 3), + ]); + let daily_metrics_right_gap: Vec = daily_mocked_metrics(vec![ + gap.clone(), + MockedMetrics::new(1, 6, 4), + MockedMetrics::new(1, 7, 3), + ]); + + assert_eq!( + assigned_nodes_multiplier(&daily_metrics_mid_gap, daily_metrics_mid_gap.len() as u64).0, + dec!(0.7866666666666666666666666667) + ); + + assert_eq!( + assigned_nodes_multiplier(&daily_metrics_mid_gap, daily_metrics_mid_gap.len() as u64).0, + assigned_nodes_multiplier(&daily_metrics_left_gap, daily_metrics_left_gap.len() as u64) + .0 + ); + assert_eq!( + assigned_nodes_multiplier( + &daily_metrics_right_gap, + daily_metrics_right_gap.len() as u64 + ) + .0, + assigned_nodes_multiplier(&daily_metrics_left_gap, daily_metrics_left_gap.len() as u64) + .0 + ); + } + + #[test] + fn test_same_rewards_if_reversed() { + let daily_metrics: Vec = daily_mocked_metrics(vec![ + MockedMetrics::new(1, 5, 5), + MockedMetrics::new(5, 6, 4), + MockedMetrics::new(25, 10, 0), + ]); + + let mut daily_metrics = daily_metrics.clone(); + let result = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + daily_metrics.reverse(); + let result_rev = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + + assert_eq!(result.0, dec!(1.0)); + assert_eq!(result_rev.0, result.0); + } + + #[test] + fn test_np_rewards_other_type() { + let mut assigned_multipliers: AHashMap> = + AHashMap::default(); + let mut rewardable_nodes: AHashMap = AHashMap::default(); + + let region_node_type = ("A,B,C".to_string(), "type0".to_string()); + + // 4 nodes in period: 2 assigned, 2 unassigned + rewardable_nodes.insert(region_node_type.clone(), 4); + assigned_multipliers.insert(region_node_type.clone(), vec![dec!(0.5), dec!(0.5)]); + + let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); + let rewards = node_provider_rewards( + &assigned_multipliers, + &rewardable_nodes, + &node_rewards_table, + ); + + // Total XDR no penalties, operation=sum(1000,1000,1000,1000), result=4000 + assert_eq!(rewards.xdr_permyriad_no_reduction, 4000); + + // Total XDR, operation=sum(1000 * 0.5,1000 * 0.5,1000 * 0.5,1000 * 0.5), result=2000 + assert_eq!(rewards.xdr_permyriad, 2000); + } + + #[test] + fn test_np_rewards_type3_coeff() { + let mut assigned_multipliers: AHashMap> = + AHashMap::default(); + let mut rewardable_nodes: AHashMap = AHashMap::default(); + let region_node_type = ("A,B,C".to_string(), "type3.1".to_string()); + + // 4 nodes in period: 1 assigned, 3 unassigned + rewardable_nodes.insert(region_node_type.clone(), 4); + assigned_multipliers.insert(region_node_type, vec![dec!(0.5)]); + let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); + let rewards = node_provider_rewards( + &assigned_multipliers, + &rewardable_nodes, + &node_rewards_table, + ); + + // Coefficients avg., operation=avg(0.95,0.95,0.95,0.95), result=0.95 + // Rewards avg., operation=avg(1500,1500,1500,1500), result=1500 + // Total rewards after coefficient reduction, operation=sum(1500 * 1,1500 * 0.95,1500 * 0.9025,1500 * 0.8574), result=5564 + + // Rewards average after coefficient reduction, operation=5564 / 4, result=1391 + // Total XDR no penalties, operation=sum(1391,1391,1391,1391), result=5564 + assert_eq!(rewards.xdr_permyriad_no_reduction, 5564); + + // Total XDR, operation=sum(1391 * 0.5,1391 * 0.5,1391 * 0.5,1391 * 0.5), result=2782 + assert_eq!(rewards.xdr_permyriad, 2782); + } + + #[test] + fn test_np_rewards_type3_mix() { + let mut assigned_multipliers: AHashMap> = + AHashMap::default(); + let mut rewardable_nodes: AHashMap = AHashMap::default(); + + // 5 nodes in period: 2 assigned, 3 unassigned + assigned_multipliers.insert( + ("A,B,D".to_string(), "type3".to_string()), + vec![dec!(0.5), dec!(0.4)], + ); + + // This will take rates from outer + rewardable_nodes.insert(("A,B,D".to_string(), "type3".to_string()), 3); + + // This will take rates from inner + rewardable_nodes.insert(("A,B,C".to_string(), "type3.1".to_string()), 2); + + let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); + let rewards = node_provider_rewards( + &assigned_multipliers, + &rewardable_nodes, + &node_rewards_table, + ); + + // Coefficients avg(0.95,0.95,0.97,0.97,0.97) = 0.9620 + // Rewards avg., operation=avg(1500,1500,1000,1000,1000), result=1200 + // Rewards average sum(1200 * 1,1200 * 0.9620,1200 * 0.9254,1200 * 0.8903,1200 * 0.8564) / 5, result=1112 + // Unassigned Nodes Multiplier, operation=avg(0.5,0.4), result=0.450 + + // Total XDR, operation=sum(1112 * 0.450,1112 * 0.450,1112 * 0.5,1112 * 0.4,1112 * 0.450), result=2502 + assert_eq!(rewards.xdr_permyriad, 2502); + // Total XDR no penalties, operation=1112 * 5, result=5561 + assert_eq!(rewards.xdr_permyriad_no_reduction, 5561); + } +} diff --git a/rs/registry/node_provider_rewards/src/v1_types.rs b/rs/registry/node_provider_rewards/src/v1_types.rs new file mode 100644 index 00000000000..d2b7149ec15 --- /dev/null +++ b/rs/registry/node_provider_rewards/src/v1_types.rs @@ -0,0 +1,114 @@ +use std::{ + collections::{HashMap, HashSet}, + hash::BuildHasherDefault, +}; + +use candid::CandidType; +use ic_base_types::PrincipalId; +use ic_management_canister_types::{NodeMetrics, NodeMetricsHistoryResponse}; +use serde::Deserialize; + +use crate::v1_logs::RewardsLog; + +pub type NodeMultiplierStats = (PrincipalId, MultiplierStats); +pub type RewardablesWithNodesMetrics = ( + AHashMap, + AHashMap>, +); +pub type RegionNodeTypeCategory = (String, String); +pub type TimestampNanos = u64; +pub type AHashSet = HashSet>; +pub type AHashMap = HashMap>; + +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct RewardableNode { + pub node_id: PrincipalId, + pub node_provider_id: PrincipalId, + pub region: String, + pub node_type: String, + pub node_metrics: Option>, +} + +#[derive(Clone, Hash, Eq, PartialEq)] +pub struct DailyNodeMetrics { + pub num_blocks_proposed: u64, + pub num_blocks_failed: u64, +} + +pub struct NodesMetricsHistory(Vec); + +impl From for AHashMap> { + fn from(nodes_metrics: NodesMetricsHistory) -> Self { + let mut sorted_metrics = nodes_metrics.0; + sorted_metrics.sort_by_key(|metrics| metrics.timestamp_nanos); + let mut sorted_metrics_per_node: AHashMap> = + AHashMap::default(); + + for metrics in sorted_metrics { + for node_metrics in metrics.node_metrics { + sorted_metrics_per_node + .entry(node_metrics.node_id) + .or_default() + .push(node_metrics); + } + } + + sorted_metrics_per_node + .into_iter() + .map(|(node_id, metrics)| { + let mut daily_node_metrics = Vec::new(); + let mut previous_proposed_total = 0; + let mut previous_failed_total = 0; + + for node_metrics in metrics { + let current_proposed_total = node_metrics.num_blocks_proposed_total; + let current_failed_total = node_metrics.num_block_failures_total; + + let (num_blocks_proposed, num_blocks_failed) = if previous_failed_total + > current_failed_total + || previous_proposed_total > current_proposed_total + { + // This is the case when node is deployed again + (current_proposed_total, current_failed_total) + } else { + ( + current_proposed_total - previous_proposed_total, + current_failed_total - previous_failed_total, + ) + }; + + daily_node_metrics.push(DailyNodeMetrics { + num_blocks_proposed, + num_blocks_failed, + }); + + previous_proposed_total = num_blocks_proposed; + previous_failed_total = num_blocks_failed; + } + (node_id, daily_node_metrics) + }) + .collect() + } +} + +#[derive(Debug, Clone, Deserialize, CandidType)] +pub struct MultiplierStats { + pub days_assigned: u64, + pub days_unassigned: u64, + pub rewards_reduction: f64, + pub blocks_failed: u64, + pub blocks_proposed: u64, + pub blocks_total: u64, + pub failure_rate: f64, +} + +pub struct RewardsPerNodeProvider { + pub rewards_per_node_provider: AHashMap)>, + pub rewards_log_per_node_provider: AHashMap, +} + +#[derive(Debug, Clone)] +pub struct Rewards { + pub xdr_permyriad: u64, + pub xdr_permyriad_no_reduction: u64, +} From 9953d891226c210537a04eae49aab67da53157d2 Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 18 Nov 2024 15:20:16 +0100 Subject: [PATCH 02/20] Add v1 modules --- .../get_node_providers_monthly_xdr_rewards.rs | 4 +- rs/registry/node_provider_rewards/src/lib.rs | 160 +++++++++++++++++- .../src/{v0_logs.rs => logs.rs} | 0 .../node_provider_rewards/src/v0_rewards.rs | 159 ----------------- 4 files changed, 160 insertions(+), 163 deletions(-) rename rs/registry/node_provider_rewards/src/{v0_logs.rs => logs.rs} (100%) delete mode 100644 rs/registry/node_provider_rewards/src/v0_rewards.rs diff --git a/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs b/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs index 2de971c70fe..7c099c70621 100644 --- a/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs +++ b/rs/registry/canister/src/get_node_providers_monthly_xdr_rewards.rs @@ -10,7 +10,7 @@ use ic_protobuf::registry::{ use ic_registry_keys::{ DATA_CENTER_KEY_PREFIX, NODE_OPERATOR_RECORD_KEY_PREFIX, NODE_REWARDS_TABLE_KEY, }; -use ic_registry_node_provider_rewards::v0_rewards::calculate_rewards; +use ic_registry_node_provider_rewards::calculate_rewards_v0; use prost::Message; use std::collections::BTreeMap; @@ -37,7 +37,7 @@ impl Registry { let data_centers = get_key_family_iter::(self, DATA_CENTER_KEY_PREFIX) .collect::>(); - let reward_values = calculate_rewards(&rewards_table, &node_operators, &data_centers)?; + let reward_values = calculate_rewards_v0(&rewards_table, &node_operators, &data_centers)?; rewards.rewards = reward_values .rewards_per_node_provider diff --git a/rs/registry/node_provider_rewards/src/lib.rs b/rs/registry/node_provider_rewards/src/lib.rs index 78b0ac08e5a..5db103ba36a 100644 --- a/rs/registry/node_provider_rewards/src/lib.rs +++ b/rs/registry/node_provider_rewards/src/lib.rs @@ -1,7 +1,163 @@ +use crate::logs::{LogEntry, RewardsPerNodeProviderLog}; +use ic_base_types::PrincipalId; +use ic_protobuf::registry::{ + dc::v1::DataCenterRecord, + node_operator::v1::NodeOperatorRecord, + node_rewards::v2::{NodeRewardRate, NodeRewardsTable}, +}; +use std::collections::{BTreeMap, HashMap}; +pub mod logs; pub mod v1_logs; pub mod v1_rewards; pub mod v1_types; -pub mod v0_logs; +pub struct RewardsPerNodeProvider { + pub rewards_per_node_provider: BTreeMap, + pub computation_log: BTreeMap, +} -pub mod v0_rewards; +pub fn calculate_rewards_v0( + rewards_table: &NodeRewardsTable, + node_operators: &[(String, NodeOperatorRecord)], + data_centers: &BTreeMap, +) -> Result { + // The reward coefficients for the NP, at the moment used only for type3 nodes, as a measure for stimulating decentralization. + // It is kept outside of the reward calculation loop in order to reduce node rewards for NPs with multiple DCs. + // We want to have as many independent NPs as possible for the given reward budget. + let mut np_coefficients: HashMap = HashMap::new(); + + let mut rewards = BTreeMap::new(); + let mut computation_log = BTreeMap::new(); + + for (key_string, node_operator) in node_operators.iter() { + let node_operator_id = PrincipalId::try_from(&node_operator.node_operator_principal_id) + .map_err(|e| { + format!( + "Node Operator key '{:?}' cannot be parsed as a PrincipalId: '{}'", + key_string, e + ) + })?; + + let node_provider_id = PrincipalId::try_from(&node_operator.node_provider_principal_id) + .map_err(|e| { + format!( + "Node Operator with key '{}' has a node_provider_principal_id \ + that cannot be parsed as a PrincipalId: '{}'", + node_operator_id, e + ) + })?; + + let dc = data_centers.get(&node_operator.dc_id).ok_or_else(|| { + format!( + "Node Operator with key '{}' has data center ID '{}' \ + not found in the Registry", + node_operator_id, node_operator.dc_id + ) + })?; + let region = &dc.region; + + let np_rewards = rewards.entry(node_provider_id).or_default(); + let np_log = computation_log + .entry(node_provider_id) + .or_insert(RewardsPerNodeProviderLog::new(node_provider_id)); + + for (node_type, node_count) in node_operator.rewardable_nodes.iter() { + let rate = match rewards_table.get_rate(region, node_type) { + Some(rate) => rate, + None => { + np_log.add_entry(LogEntry::RateNotFoundInRewardTable { + region: region.clone(), + node_type: node_type.clone(), + node_operator_id, + }); + + NodeRewardRate { + xdr_permyriad_per_node_per_month: 1, + reward_coefficient_percent: Some(100), + } + } + }; + + let dc_reward = match &node_type { + t if t.starts_with("type3") => { + // For type3 nodes, the rewards are progressively reduced for each additional node owned by a NP. + // This helps to improve network decentralization. The first node gets the full reward. + // After the first node, the rewards are progressively reduced by multiplying them with reward_coefficient_percent. + // For the n-th node, the reward is: + // reward(n) = reward(n-1) * reward_coefficient_percent ^ (n-1) + // + // A note around the type3 rewards and iter() over self.store + // + // One known issue with this implementation is that in some edge cases it could lead to + // unexpected results. The outer loop iterates over the node operator records sorted + // lexicographically, instead of the order in which the records were added to the registry, + // or instead of the order in which NP/NO adds nodes to the network. This means that all + // reduction factors for the node operator A are applied prior to all reduction factors for + // the node operator B, independently from the order in which the node operator records, + // nodes, or the rewardable nodes were added to the registry. + // For instance, say a Node Provider adds a Node Operator B in region 1 with higher reward + // coefficient so higher average rewards, and then A in region 2 with lower reward + // coefficient so lower average rewards. When the rewards are calculated, the rewards for + // Node Operator A are calculated before the rewards for B (due to the lexicographical + // order), and the final rewards will be lower than they would be calculated first for B and + // then for A, as expected based on the insert order. + + let reward_base = rate.xdr_permyriad_per_node_per_month as f64; + + // To de-stimulate the same NP having too many nodes in the same country, the node rewards + // is reduced for each node the NP has in the given country. + // Join the NP PrincipalId + DC Continent + DC Country, and use that as the key for the + // reduction coefficients. + let np_coefficients_key = format!( + "{}:{}", + node_provider_id, + region + .splitn(3, ',') + .take(2) + .collect::>() + .join(":") + ); + + let mut np_coeff = *np_coefficients.get(&np_coefficients_key).unwrap_or(&1.0); + + // Default reward_coefficient_percent is set to 80%, which is used as a fallback only in the + // unlikely case that the type3 entry in the reward table: + // a) has xdr_permyriad_per_node_per_month entry set for this region, but + // b) does NOT have the reward_coefficient_percent value set + let dc_reward_coefficient_percent = + rate.reward_coefficient_percent.unwrap_or(80) as f64 / 100.0; + + let mut dc_reward = 0; + for i in 0..*node_count { + let node_reward = (reward_base * np_coeff) as u64; + np_log.add_entry(LogEntry::NodeRewards { + node_type: node_type.clone(), + node_idx: i, + dc_id: node_operator.dc_id.clone(), + rewardable_count: *node_count, + rewards_xdr_permyriad: node_reward, + }); + dc_reward += node_reward; + np_coeff *= dc_reward_coefficient_percent; + } + np_coefficients.insert(np_coefficients_key, np_coeff); + dc_reward + } + _ => *node_count as u64 * rate.xdr_permyriad_per_node_per_month, + }; + + np_log.add_entry(LogEntry::DCRewards { + dc_id: node_operator.dc_id.clone(), + node_type: node_type.clone(), + rewardable_count: *node_count, + rewards_xdr_permyriad: dc_reward, + }); + *np_rewards += dc_reward; + } + } + + Ok(RewardsPerNodeProvider { + rewards_per_node_provider: rewards, + computation_log, + }) +} diff --git a/rs/registry/node_provider_rewards/src/v0_logs.rs b/rs/registry/node_provider_rewards/src/logs.rs similarity index 100% rename from rs/registry/node_provider_rewards/src/v0_logs.rs rename to rs/registry/node_provider_rewards/src/logs.rs diff --git a/rs/registry/node_provider_rewards/src/v0_rewards.rs b/rs/registry/node_provider_rewards/src/v0_rewards.rs deleted file mode 100644 index f2cfc0fb8cb..00000000000 --- a/rs/registry/node_provider_rewards/src/v0_rewards.rs +++ /dev/null @@ -1,159 +0,0 @@ -use crate::v0_logs::{LogEntry, RewardsPerNodeProviderLog}; -use ic_base_types::PrincipalId; -use ic_protobuf::registry::{ - dc::v1::DataCenterRecord, - node_operator::v1::NodeOperatorRecord, - node_rewards::v2::{NodeRewardRate, NodeRewardsTable}, -}; -use std::collections::{BTreeMap, HashMap}; - -pub struct RewardsPerNodeProvider { - pub rewards_per_node_provider: BTreeMap, - pub computation_log: BTreeMap, -} - -pub fn calculate_rewards( - rewards_table: &NodeRewardsTable, - node_operators: &[(String, NodeOperatorRecord)], - data_centers: &BTreeMap, -) -> Result { - // The reward coefficients for the NP, at the moment used only for type3 nodes, as a measure for stimulating decentralization. - // It is kept outside of the reward calculation loop in order to reduce node rewards for NPs with multiple DCs. - // We want to have as many independent NPs as possible for the given reward budget. - let mut np_coefficients: HashMap = HashMap::new(); - - let mut rewards = BTreeMap::new(); - let mut computation_log = BTreeMap::new(); - - for (key_string, node_operator) in node_operators.iter() { - let node_operator_id = PrincipalId::try_from(&node_operator.node_operator_principal_id) - .map_err(|e| { - format!( - "Node Operator key '{:?}' cannot be parsed as a PrincipalId: '{}'", - key_string, e - ) - })?; - - let node_provider_id = PrincipalId::try_from(&node_operator.node_provider_principal_id) - .map_err(|e| { - format!( - "Node Operator with key '{}' has a node_provider_principal_id \ - that cannot be parsed as a PrincipalId: '{}'", - node_operator_id, e - ) - })?; - - let dc = data_centers.get(&node_operator.dc_id).ok_or_else(|| { - format!( - "Node Operator with key '{}' has data center ID '{}' \ - not found in the Registry", - node_operator_id, node_operator.dc_id - ) - })?; - let region = &dc.region; - - let np_rewards = rewards.entry(node_provider_id).or_default(); - let np_log = computation_log - .entry(node_provider_id) - .or_insert(RewardsPerNodeProviderLog::new(node_provider_id)); - - for (node_type, node_count) in node_operator.rewardable_nodes.iter() { - let rate = match rewards_table.get_rate(region, node_type) { - Some(rate) => rate, - None => { - np_log.add_entry(LogEntry::RateNotFoundInRewardTable { - region: region.clone(), - node_type: node_type.clone(), - node_operator_id, - }); - - NodeRewardRate { - xdr_permyriad_per_node_per_month: 1, - reward_coefficient_percent: Some(100), - } - } - }; - - let dc_reward = match &node_type { - t if t.starts_with("type3") => { - // For type3 nodes, the rewards are progressively reduced for each additional node owned by a NP. - // This helps to improve network decentralization. The first node gets the full reward. - // After the first node, the rewards are progressively reduced by multiplying them with reward_coefficient_percent. - // For the n-th node, the reward is: - // reward(n) = reward(n-1) * reward_coefficient_percent ^ (n-1) - // - // A note around the type3 rewards and iter() over self.store - // - // One known issue with this implementation is that in some edge cases it could lead to - // unexpected results. The outer loop iterates over the node operator records sorted - // lexicographically, instead of the order in which the records were added to the registry, - // or instead of the order in which NP/NO adds nodes to the network. This means that all - // reduction factors for the node operator A are applied prior to all reduction factors for - // the node operator B, independently from the order in which the node operator records, - // nodes, or the rewardable nodes were added to the registry. - // For instance, say a Node Provider adds a Node Operator B in region 1 with higher reward - // coefficient so higher average rewards, and then A in region 2 with lower reward - // coefficient so lower average rewards. When the rewards are calculated, the rewards for - // Node Operator A are calculated before the rewards for B (due to the lexicographical - // order), and the final rewards will be lower than they would be calculated first for B and - // then for A, as expected based on the insert order. - - let reward_base = rate.xdr_permyriad_per_node_per_month as f64; - - // To de-stimulate the same NP having too many nodes in the same country, the node rewards - // is reduced for each node the NP has in the given country. - // Join the NP PrincipalId + DC Continent + DC Country, and use that as the key for the - // reduction coefficients. - let np_coefficients_key = format!( - "{}:{}", - node_provider_id, - region - .splitn(3, ',') - .take(2) - .collect::>() - .join(":") - ); - - let mut np_coeff = *np_coefficients.get(&np_coefficients_key).unwrap_or(&1.0); - - // Default reward_coefficient_percent is set to 80%, which is used as a fallback only in the - // unlikely case that the type3 entry in the reward table: - // a) has xdr_permyriad_per_node_per_month entry set for this region, but - // b) does NOT have the reward_coefficient_percent value set - let dc_reward_coefficient_percent = - rate.reward_coefficient_percent.unwrap_or(80) as f64 / 100.0; - - let mut dc_reward = 0; - for i in 0..*node_count { - let node_reward = (reward_base * np_coeff) as u64; - np_log.add_entry(LogEntry::NodeRewards { - node_type: node_type.clone(), - node_idx: i, - dc_id: node_operator.dc_id.clone(), - rewardable_count: *node_count, - rewards_xdr_permyriad: node_reward, - }); - dc_reward += node_reward; - np_coeff *= dc_reward_coefficient_percent; - } - np_coefficients.insert(np_coefficients_key, np_coeff); - dc_reward - } - _ => *node_count as u64 * rate.xdr_permyriad_per_node_per_month, - }; - - np_log.add_entry(LogEntry::DCRewards { - dc_id: node_operator.dc_id.clone(), - node_type: node_type.clone(), - rewardable_count: *node_count, - rewards_xdr_permyriad: dc_reward, - }); - *np_rewards += dc_reward; - } - } - - Ok(RewardsPerNodeProvider { - rewards_per_node_provider: rewards, - computation_log, - }) -} From 25257bfd6d74512f36a4e70027c5506cf8d06aff Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 18 Nov 2024 15:45:29 +0100 Subject: [PATCH 03/20] Fix bazel run --- rs/registry/node_provider_rewards/BUILD.bazel | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/rs/registry/node_provider_rewards/BUILD.bazel b/rs/registry/node_provider_rewards/BUILD.bazel index 866b7e44949..4202af0c786 100644 --- a/rs/registry/node_provider_rewards/BUILD.bazel +++ b/rs/registry/node_provider_rewards/BUILD.bazel @@ -4,11 +4,16 @@ package(default_visibility = ["//visibility:public"]) DEPENDENCIES = [ "//rs/types/base_types", + "//rs/types/management_canister_types", "//rs/protobuf", -] - -DEV_DEPENDENCIES = [ - "@crate_index//:maplit", + "@crate_index//:ahash", + "@crate_index//:num-traits", + "@crate_index//:itertools", + "@crate_index//:lazy_static", + "@crate_index//:rust_decimal", + "@crate_index//:rust_decimal_macros", + "@crate_index//:serde", + "@crate_index//:candid", ] rust_library( @@ -22,5 +27,5 @@ rust_library( rust_test( name = "node_provider_rewards_test", crate = ":node_provider_rewards", - deps = DEPENDENCIES + DEV_DEPENDENCIES, + deps = DEPENDENCIES, ) From c5424dd3e76be5ce9e89bfe16298d3174815eef2 Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 18 Nov 2024 15:20:19 +0000 Subject: [PATCH 04/20] Fix bazel dep --- Cargo.Bazel.json.lock | 270 +++++++++++++++++- Cargo.Bazel.toml.lock | 22 ++ bazel/external_crates.bzl | 7 + rs/registry/node_provider_rewards/BUILD.bazel | 6 +- 4 files changed, 289 insertions(+), 16 deletions(-) diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index c291c35f2f5..e921ba24326 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "3ae1d8f974c70715eb6c96b5461fb094433a0ac382617ef84fa0efbfb2a8feef", + "checksum": "ce1d8efc0f765dda5293531947a959f7307874f34dec4f62835a0df314b1693f", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -1469,6 +1469,8 @@ ], "crate_features": { "common": [ + "compile-time-rng", + "const-random", "default", "getrandom", "runtime-rng", @@ -1486,6 +1488,10 @@ "id": "cfg-if 1.0.0", "target": "cfg_if" }, + { + "id": "const-random 0.1.18", + "target": "const_random" + }, { "id": "getrandom 0.2.10", "target": "getrandom" @@ -13467,6 +13473,110 @@ ], "license_file": "LICENSE-APACHE" }, + "const-random 0.1.18": { + "name": "const-random", + "version": "0.1.18", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random/0.1.18/download", + "sha256": "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" + } + }, + "targets": [ + { + "Library": { + "crate_name": "const_random", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "const-random-macro 0.1.16", + "target": "const_random_macro" + } + ], + "selects": {} + }, + "version": "0.1.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "const-random-macro 0.1.16": { + "name": "const-random-macro", + "version": "0.1.16", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random-macro/0.1.16/download", + "sha256": "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "const_random_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "getrandom 0.2.10", + "target": "getrandom" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "tiny-keccak 2.0.2", + "target": "tiny_keccak" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.16" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "convert_case 0.4.0": { "name": "convert_case", "version": "0.4.0", @@ -18116,6 +18226,10 @@ "id": "addr 0.15.6", "target": "addr" }, + { + "id": "ahash 0.8.11", + "target": "ahash" + }, { "id": "aide 0.13.4", "target": "aide" @@ -53064,6 +53178,12 @@ "target": "libc" } ], + "aarch64-pc-windows-msvc": [ + { + "id": "libc 0.2.158", + "target": "libc" + } + ], "aarch64-unknown-linux-gnu": [ { "id": "libc 0.2.158", @@ -53112,6 +53232,12 @@ "target": "libc" } ], + "i686-pc-windows-msvc": [ + { + "id": "libc 0.2.158", + "target": "libc" + } + ], "i686-unknown-freebsd": [ { "id": "libc 0.2.158", @@ -53160,6 +53286,12 @@ "target": "libc" } ], + "x86_64-pc-windows-msvc": [ + { + "id": "libc 0.2.158", + "target": "libc" + } + ], "x86_64-unknown-freebsd": [ { "id": "libc 0.2.158", @@ -60125,6 +60257,11 @@ "time", "use-libc-auxv" ], + "aarch64-pc-windows-msvc": [ + "default", + "termios", + "use-libc-auxv" + ], "aarch64-unknown-linux-gnu": [ "default", "event", @@ -60217,6 +60354,11 @@ "time", "use-libc-auxv" ], + "i686-pc-windows-msvc": [ + "default", + "termios", + "use-libc-auxv" + ], "i686-unknown-freebsd": [ "default", "event", @@ -60331,6 +60473,11 @@ "time", "use-libc-auxv" ], + "x86_64-pc-windows-msvc": [ + "default", + "termios", + "use-libc-auxv" + ], "x86_64-unknown-freebsd": [ "default", "event", @@ -60446,6 +60593,32 @@ "id": "errno 0.3.8", "target": "errno", "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" + } + ], + "aarch64-unknown-linux-gnu": [ + { + "id": "errno 0.3.8", + "target": "errno", + "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" + } + ], + "aarch64-unknown-nixos-gnu": [ + { + "id": "errno 0.3.8", + "target": "errno", + "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" } ], "aarch64-unknown-nto-qnx710": [ @@ -60459,6 +60632,17 @@ "target": "libc" } ], + "arm-unknown-linux-gnueabi": [ + { + "id": "errno 0.3.8", + "target": "errno", + "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" + } + ], "armv7-linux-androideabi": [ { "id": "errno 0.3.8", @@ -60531,6 +60715,10 @@ "id": "errno 0.3.8", "target": "errno", "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" } ], "i686-unknown-freebsd": [ @@ -60544,6 +60732,17 @@ "target": "libc" } ], + "i686-unknown-linux-gnu": [ + { + "id": "errno 0.3.8", + "target": "errno", + "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" + } + ], "powerpc-unknown-linux-gnu": [ { "id": "errno 0.3.8", @@ -60681,6 +60880,10 @@ "id": "errno 0.3.8", "target": "errno", "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" } ], "x86_64-unknown-freebsd": [ @@ -60694,6 +60897,28 @@ "target": "libc" } ], + "x86_64-unknown-linux-gnu": [ + { + "id": "errno 0.3.8", + "target": "errno", + "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" + } + ], + "x86_64-unknown-nixos-gnu": [ + { + "id": "errno 0.3.8", + "target": "errno", + "alias": "libc_errno" + }, + { + "id": "libc 0.2.158", + "target": "libc" + } + ], "x86_64-unknown-none": [ { "id": "errno 0.3.8", @@ -71517,46 +71742,60 @@ ], "selects": { "aarch64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "aarch64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ], "arm-unknown-linux-gnueabi": [ - "sha3" + "sha3", + "shake" ], "i686-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "i686-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "powerpc-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "s390x-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "x86_64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-freebsd": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ] } }, @@ -86137,6 +86376,7 @@ "actix-rt 2.10.0", "actix-web 4.9.0", "addr 0.15.6", + "ahash 0.8.11", "aide 0.13.4", "anyhow 1.0.93", "arbitrary 1.3.2", diff --git a/Cargo.Bazel.toml.lock b/Cargo.Bazel.toml.lock index 81989f7a403..758db7dcbdf 100644 --- a/Cargo.Bazel.toml.lock +++ b/Cargo.Bazel.toml.lock @@ -276,6 +276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if 1.0.0", + "const-random", "getrandom", "once_cell", "version_check", @@ -2231,6 +2232,26 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "795bc6e66a8e340f075fcf6227e417a2dc976b92b91f3cdc778bb858778b6747" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -2947,6 +2968,7 @@ dependencies = [ "actix-rt", "actix-web", "addr", + "ahash 0.8.11", "aide", "anyhow", "arbitrary", diff --git a/bazel/external_crates.bzl b/bazel/external_crates.bzl index a4326931f7a..9a3db6ff439 100644 --- a/bazel/external_crates.bzl +++ b/bazel/external_crates.bzl @@ -171,6 +171,13 @@ def external_crates_repository(name, cargo_lockfile, lockfile, sanitizers_enable "idna", ], ), + "ahash": crate.spec( + version = "^0.8.11", + default_features = False, + features = [ + "compile-time-rng", + ], + ), "aide": crate.spec( version = "^0.13.4", features = [ diff --git a/rs/registry/node_provider_rewards/BUILD.bazel b/rs/registry/node_provider_rewards/BUILD.bazel index 4202af0c786..38e8f4bfa6a 100644 --- a/rs/registry/node_provider_rewards/BUILD.bazel +++ b/rs/registry/node_provider_rewards/BUILD.bazel @@ -11,16 +11,20 @@ DEPENDENCIES = [ "@crate_index//:itertools", "@crate_index//:lazy_static", "@crate_index//:rust_decimal", - "@crate_index//:rust_decimal_macros", "@crate_index//:serde", "@crate_index//:candid", ] +MACRO_DEPENDENCIES = [ + "@crate_index//:rust_decimal_macros" +] + rust_library( name = "node_provider_rewards", srcs = glob(["src/**/*.rs"]), crate_name = "ic_registry_node_provider_rewards", version = "0.9.0", + proc_macro_deps = MACRO_DEPENDENCIES, deps = DEPENDENCIES, ) From 64a24f2317235ef965a25d18bcba6f2d2f735782 Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 18 Nov 2024 18:50:29 +0100 Subject: [PATCH 05/20] Fix bazel --- rs/registry/node_provider_rewards/BUILD.bazel | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/registry/node_provider_rewards/BUILD.bazel b/rs/registry/node_provider_rewards/BUILD.bazel index 38e8f4bfa6a..967402130c9 100644 --- a/rs/registry/node_provider_rewards/BUILD.bazel +++ b/rs/registry/node_provider_rewards/BUILD.bazel @@ -16,15 +16,15 @@ DEPENDENCIES = [ ] MACRO_DEPENDENCIES = [ - "@crate_index//:rust_decimal_macros" + "@crate_index//:rust_decimal_macros", ] rust_library( name = "node_provider_rewards", srcs = glob(["src/**/*.rs"]), crate_name = "ic_registry_node_provider_rewards", - version = "0.9.0", proc_macro_deps = MACRO_DEPENDENCIES, + version = "0.9.0", deps = DEPENDENCIES, ) From 5b1cf8d8bb245c8b9a31dc2bc6fba871dca0a9d8 Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 18 Nov 2024 19:55:38 +0100 Subject: [PATCH 06/20] Fix bazel --- Cargo.Bazel.json.lock | 113 +----------------------------------------- 1 file changed, 1 insertion(+), 112 deletions(-) diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index e921ba24326..7f76b2cf34b 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "ce1d8efc0f765dda5293531947a959f7307874f34dec4f62835a0df314b1693f", + "checksum": "8937cce18e46d622f1599c306222ba267f9eb27f581e612982bb27d87bf4cf0d", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -53178,12 +53178,6 @@ "target": "libc" } ], - "aarch64-pc-windows-msvc": [ - { - "id": "libc 0.2.158", - "target": "libc" - } - ], "aarch64-unknown-linux-gnu": [ { "id": "libc 0.2.158", @@ -53232,12 +53226,6 @@ "target": "libc" } ], - "i686-pc-windows-msvc": [ - { - "id": "libc 0.2.158", - "target": "libc" - } - ], "i686-unknown-freebsd": [ { "id": "libc 0.2.158", @@ -53286,12 +53274,6 @@ "target": "libc" } ], - "x86_64-pc-windows-msvc": [ - { - "id": "libc 0.2.158", - "target": "libc" - } - ], "x86_64-unknown-freebsd": [ { "id": "libc 0.2.158", @@ -60257,11 +60239,6 @@ "time", "use-libc-auxv" ], - "aarch64-pc-windows-msvc": [ - "default", - "termios", - "use-libc-auxv" - ], "aarch64-unknown-linux-gnu": [ "default", "event", @@ -60354,11 +60331,6 @@ "time", "use-libc-auxv" ], - "i686-pc-windows-msvc": [ - "default", - "termios", - "use-libc-auxv" - ], "i686-unknown-freebsd": [ "default", "event", @@ -60473,11 +60445,6 @@ "time", "use-libc-auxv" ], - "x86_64-pc-windows-msvc": [ - "default", - "termios", - "use-libc-auxv" - ], "x86_64-unknown-freebsd": [ "default", "event", @@ -60593,32 +60560,6 @@ "id": "errno 0.3.8", "target": "errno", "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" - } - ], - "aarch64-unknown-linux-gnu": [ - { - "id": "errno 0.3.8", - "target": "errno", - "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" - } - ], - "aarch64-unknown-nixos-gnu": [ - { - "id": "errno 0.3.8", - "target": "errno", - "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" } ], "aarch64-unknown-nto-qnx710": [ @@ -60632,17 +60573,6 @@ "target": "libc" } ], - "arm-unknown-linux-gnueabi": [ - { - "id": "errno 0.3.8", - "target": "errno", - "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" - } - ], "armv7-linux-androideabi": [ { "id": "errno 0.3.8", @@ -60715,10 +60645,6 @@ "id": "errno 0.3.8", "target": "errno", "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" } ], "i686-unknown-freebsd": [ @@ -60732,17 +60658,6 @@ "target": "libc" } ], - "i686-unknown-linux-gnu": [ - { - "id": "errno 0.3.8", - "target": "errno", - "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" - } - ], "powerpc-unknown-linux-gnu": [ { "id": "errno 0.3.8", @@ -60880,10 +60795,6 @@ "id": "errno 0.3.8", "target": "errno", "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" } ], "x86_64-unknown-freebsd": [ @@ -60897,28 +60808,6 @@ "target": "libc" } ], - "x86_64-unknown-linux-gnu": [ - { - "id": "errno 0.3.8", - "target": "errno", - "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" - } - ], - "x86_64-unknown-nixos-gnu": [ - { - "id": "errno 0.3.8", - "target": "errno", - "alias": "libc_errno" - }, - { - "id": "libc 0.2.158", - "target": "libc" - } - ], "x86_64-unknown-none": [ { "id": "errno 0.3.8", From fc54dd24fa0fb8b6099e1f8d4a5e054770cfb9f4 Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 18 Nov 2024 21:45:33 +0100 Subject: [PATCH 07/20] Fix bazel --- Cargo.Bazel.Fuzzing.json.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.Bazel.Fuzzing.json.lock b/Cargo.Bazel.Fuzzing.json.lock index 1386a5decdd..6adf2a7b5ea 100644 --- a/Cargo.Bazel.Fuzzing.json.lock +++ b/Cargo.Bazel.Fuzzing.json.lock @@ -1,5 +1,5 @@ { - "checksum": "12851c6ba4c9f6ac1c749cc7ff0dbfe30e4f2dda7951663e4a4cc64d29fdee95", + "checksum": "9f2df98d8d1fd926a31c157d4a1d531a4ea2743880bc4d424878a2c12e1806fb", "crates": { "abnf 0.12.0": { "name": "abnf", From 46d59492c799eb5a4df1faac3e6d4b141a26dae6 Mon Sep 17 00:00:00 2001 From: Pietro Date: Thu, 28 Nov 2024 08:20:48 +0100 Subject: [PATCH 08/20] Use standard HashMap and create new logger each NP --- Cargo.lock | 22 -- rs/registry/node_provider_rewards/Cargo.toml | 3 - .../node_provider_rewards/src/v1_rewards.rs | 358 ++++++++++-------- .../node_provider_rewards/src/v1_types.rs | 21 +- 4 files changed, 203 insertions(+), 201 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98a50aa6d5d..8c838d808e3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,7 +275,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if 1.0.0", - "const-random", "getrandom", "once_cell", "version_check", @@ -2552,26 +2551,6 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" -[[package]] -name = "const-random" -version = "0.1.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" -dependencies = [ - "const-random-macro", -] - -[[package]] -name = "const-random-macro" -version = "0.1.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" -dependencies = [ - "getrandom", - "once_cell", - "tiny-keccak", -] - [[package]] name = "convert_case" version = "0.4.0" @@ -11169,7 +11148,6 @@ dependencies = [ name = "ic-registry-node-provider-rewards" version = "0.9.0" dependencies = [ - "ahash 0.8.11", "candid", "ic-base-types", "ic-management-canister-types", diff --git a/rs/registry/node_provider_rewards/Cargo.toml b/rs/registry/node_provider_rewards/Cargo.toml index 81242876b4e..4f784c384d1 100644 --- a/rs/registry/node_provider_rewards/Cargo.toml +++ b/rs/registry/node_provider_rewards/Cargo.toml @@ -8,9 +8,6 @@ edition.workspace = true [dependencies] ic-base-types = { path = "../../types/base_types" } -ahash = { version = "0.8.11", default-features = false, features = [ - "compile-time-rng", -] } ic-protobuf = { path = "../../protobuf" } itertools = { workspace = true } lazy_static = { workspace = true } diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index 6d60b18a373..1b648e834d7 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -1,78 +1,71 @@ use ic_base_types::PrincipalId; use ic_protobuf::registry::node_rewards::v2::{NodeRewardRate, NodeRewardsTable}; use itertools::Itertools; -use lazy_static::lazy_static; use num_traits::ToPrimitive; use crate::{ v1_logs::{LogEntry, Operation, RewardsLog}, v1_types::{ - AHashMap, DailyNodeMetrics, MultiplierStats, NodeMultiplierStats, RegionNodeTypeCategory, + DailyNodeMetrics, MultiplierStats, NodeMultiplierStats, RegionNodeTypeCategory, RewardableNode, RewardablesWithNodesMetrics, Rewards, RewardsPerNodeProvider, }, }; use rust_decimal::Decimal; use rust_decimal_macros::dec; -use std::{ - mem, - sync::{Arc, RwLock}, -}; +use std::collections::HashMap; const FULL_REWARDS_MACHINES_LIMIT: u32 = 4; const MIN_FAILURE_RATE: Decimal = dec!(0.1); const MAX_FAILURE_RATE: Decimal = dec!(0.6); - const RF: &str = "Linear Reduction factor"; -lazy_static! { - static ref LOGGER: Arc> = Arc::new(RwLock::new(RewardsLog::default())); -} +pub fn calculate_rewards( + days_in_period: u64, + rewards_table: &NodeRewardsTable, + rewardable_nodes: &[RewardableNode], +) -> RewardsPerNodeProvider { + let mut rewards_per_node_provider = HashMap::default(); + let mut rewards_log_per_node_provider = HashMap::default(); + let node_provider_rewardables = node_providers_rewardables(rewardable_nodes); -fn logger() -> std::sync::RwLockWriteGuard<'static, RewardsLog> { - LOGGER.write().unwrap() -} + for (node_provider_id, (rewardable_nodes, assigned_nodes_metrics)) in node_provider_rewardables + { + let mut logger = RewardsLog::default(); + let mut assigned_multipliers: HashMap> = + HashMap::default(); + let mut nodes_multiplier_stats: Vec = Vec::new(); + let total_rewardable_nodes: u32 = rewardable_nodes.values().sum(); -/// Calculates the rewards reduction based on the failure rate. -/// -/// if `failure_rate` is: -/// - Below the `MIN_FAILURE_RATE`, no reduction in rewards applied. -/// - Above the `MAX_FAILURE_RATE`, maximum reduction in rewards applied. -/// - Within the defined range (`MIN_FAILURE_RATE` to `MAX_FAILURE_RATE`), -/// the function calculates the reduction from the linear reduction function. -fn rewards_reduction_percent(failure_rate: &Decimal) -> Decimal { - if failure_rate < &MIN_FAILURE_RATE { - logger().execute( - &format!( - "No Reduction applied because {} is less than {} failure rate.\n{}", - failure_rate.round_dp(4), - MIN_FAILURE_RATE, - RF - ), - Operation::Set(dec!(0)), - ) - } else if failure_rate > &MAX_FAILURE_RATE { - logger().execute( - &format!( - "Max reduction applied because {} is over {} failure rate.\n{}", - failure_rate.round_dp(4), - MAX_FAILURE_RATE, - RF - ), - Operation::Set(dec!(0.8)), - ) - } else { - let y_change = logger().execute( - "Linear Reduction Y change", - Operation::Subtract(*failure_rate, MIN_FAILURE_RATE), - ); - let x_change = logger().execute( - "Linear Reduction X change", - Operation::Subtract(MAX_FAILURE_RATE, MIN_FAILURE_RATE), + logger.add_entry(LogEntry::RewardsForNodeProvider( + node_provider_id, + total_rewardable_nodes, + )); + + for (node, daily_metrics) in assigned_nodes_metrics { + let (multiplier, multiplier_stats) = + assigned_nodes_multiplier(&mut logger, &daily_metrics, days_in_period); + logger.add_entry(LogEntry::RewardMultiplierForNode(node.node_id, multiplier)); + nodes_multiplier_stats.push((node.node_id, multiplier_stats)); + assigned_multipliers + .entry((node.region.clone(), node.node_type.clone())) + .or_default() + .push(multiplier); + } + + let rewards = node_provider_rewards( + &mut logger, + &assigned_multipliers, + &rewardable_nodes, + rewards_table, ); - let m = logger().execute("Compute m", Operation::Divide(y_change, x_change)); + rewards_log_per_node_provider.insert(node_provider_id, logger); + rewards_per_node_provider.insert(node_provider_id, (rewards, nodes_multiplier_stats)); + } - logger().execute(RF, Operation::Multiply(m, dec!(0.8))) + RewardsPerNodeProvider { + rewards_per_node_provider, + rewards_log_per_node_provider, } } @@ -85,16 +78,17 @@ fn rewards_reduction_percent(failure_rate: &Decimal) -> Decimal { /// 3. The `rewards_reduction` function is applied to `overall_failure_rate`. /// 3. Finally, the rewards multiplier to be distributed to the node is computed. pub fn assigned_nodes_multiplier( + logger: &mut RewardsLog, daily_metrics: &[DailyNodeMetrics], total_days: u64, ) -> (Decimal, MultiplierStats) { let total_days = Decimal::from(total_days); - let days_assigned = logger().execute( + let days_assigned = logger.execute( "Assigned Days In Period", Operation::Set(Decimal::from(daily_metrics.len())), ); - let days_unassigned = logger().execute( + let days_unassigned = logger.execute( "Unassigned Days In Period", Operation::Subtract(total_days, days_assigned), ); @@ -108,19 +102,19 @@ pub fn assigned_nodes_multiplier( .map(|metrics| metrics.num_blocks_proposed.into()) .collect_vec(); - let overall_failed = logger().execute( + let overall_failed = logger.execute( "Computing Total Failed Blocks", Operation::Sum(daily_failed), ); - let overall_proposed = logger().execute( + let overall_proposed = logger.execute( "Computing Total Proposed Blocks", Operation::Sum(daily_proposed), ); - let overall_total = logger().execute( + let overall_total = logger.execute( "Computing Total Blocks", Operation::Sum(vec![overall_failed, overall_proposed]), ); - let overall_failure_rate = logger().execute( + let overall_failure_rate = logger.execute( "Computing Total Failure Rate", if overall_total > dec!(0) { Operation::Divide(overall_failed, overall_total) @@ -129,26 +123,26 @@ pub fn assigned_nodes_multiplier( }, ); - let rewards_reduction = rewards_reduction_percent(&overall_failure_rate); - let rewards_multiplier_assigned = logger().execute( + let rewards_reduction = rewards_reduction_percent(logger, &overall_failure_rate); + let rewards_multiplier_assigned = logger.execute( "Reward Multiplier Assigned Days", Operation::Subtract(dec!(1), rewards_reduction), ); // On days when the node is not assigned to a subnet, it will receive the same `Reward Multiplier` as computed for the days it was assigned. - let rewards_multiplier_unassigned = logger().execute( + let rewards_multiplier_unassigned = logger.execute( "Reward Multiplier Unassigned Days", Operation::Set(rewards_multiplier_assigned), ); - let assigned_days_factor = logger().execute( + let assigned_days_factor = logger.execute( "Assigned Days Factor", Operation::Multiply(days_assigned, rewards_multiplier_assigned), ); - let unassigned_days_factor = logger().execute( + let unassigned_days_factor = logger.execute( "Unassigned Days Factor (currently equal to Assigned Days Factor)", Operation::Multiply(days_unassigned, rewards_multiplier_unassigned), ); - let rewards_multiplier = logger().execute( + let rewards_multiplier = logger.execute( "Average reward multiplier", Operation::Divide(assigned_days_factor + unassigned_days_factor, total_days), ); @@ -166,6 +160,50 @@ pub fn assigned_nodes_multiplier( (rewards_multiplier, rewards_multiplier_stats) } +/// Calculates the rewards reduction based on the failure rate. +/// +/// if `failure_rate` is: +/// - Below the `MIN_FAILURE_RATE`, no reduction in rewards applied. +/// - Above the `MAX_FAILURE_RATE`, maximum reduction in rewards applied. +/// - Within the defined range (`MIN_FAILURE_RATE` to `MAX_FAILURE_RATE`), +/// the function calculates the reduction from the linear reduction function. +fn rewards_reduction_percent(logger: &mut RewardsLog, failure_rate: &Decimal) -> Decimal { + if failure_rate < &MIN_FAILURE_RATE { + logger.execute( + &format!( + "No Reduction applied because {} is less than {} failure rate.\n{}", + failure_rate.round_dp(4), + MIN_FAILURE_RATE, + RF + ), + Operation::Set(dec!(0)), + ) + } else if failure_rate > &MAX_FAILURE_RATE { + logger.execute( + &format!( + "Max reduction applied because {} is over {} failure rate.\n{}", + failure_rate.round_dp(4), + MAX_FAILURE_RATE, + RF + ), + Operation::Set(dec!(0.8)), + ) + } else { + let y_change = logger.execute( + "Linear Reduction Y change", + Operation::Subtract(*failure_rate, MIN_FAILURE_RATE), + ); + let x_change = logger.execute( + "Linear Reduction X change", + Operation::Subtract(MAX_FAILURE_RATE, MIN_FAILURE_RATE), + ); + + let m = logger.execute("Compute m", Operation::Divide(y_change, x_change)); + + logger.execute(RF, Operation::Multiply(m, dec!(0.8))) + } +} + fn region_type3_key(region: String) -> RegionNodeTypeCategory { // The rewards table contains entries of this form DC Continent + DC Country + DC State/City. // The grouping for type3* nodes will be on DC Continent + DC Country level. This group is used for computing @@ -180,23 +218,23 @@ fn region_type3_key(region: String) -> RegionNodeTypeCategory { } fn base_rewards_region_nodetype( - rewardable_nodes: &AHashMap, + logger: &mut RewardsLog, + rewardable_nodes: &HashMap, rewards_table: &NodeRewardsTable, -) -> AHashMap { - let mut type3_coefficients_rewards: AHashMap< +) -> HashMap { + let mut type3_coefficients_rewards: HashMap< RegionNodeTypeCategory, (Vec, Vec), - > = AHashMap::default(); - let mut region_nodetype_rewards: AHashMap = - AHashMap::default(); + > = HashMap::default(); + let mut region_nodetype_rewards: HashMap = HashMap::default(); for ((region, node_type), node_count) in rewardable_nodes { let rate = match rewards_table.get_rate(region, node_type) { Some(rate) => rate, None => { - logger().add_entry(LogEntry::RateNotFoundInRewardTable { - node_type: node_type.clone(), - region: region.clone(), + logger.add_entry(LogEntry::RateNotFoundInRewardTable { + node_type: node_type.to_string(), + region: region.to_string(), }); NodeRewardRate { @@ -217,7 +255,7 @@ fn base_rewards_region_nodetype( coeff = Decimal::from(rate.reward_coefficient_percent.unwrap_or(80)) / dec!(100); let coefficients = vec![coeff; *node_count as usize]; let base_rewards = vec![base_rewards; *node_count as usize]; - let region_key = region_type3_key(region.clone()); + let region_key = region_type3_key(region.to_string()); type3_coefficients_rewards .entry(region_key) @@ -233,9 +271,9 @@ fn base_rewards_region_nodetype( region_nodetype_rewards.insert((region.clone(), node_type.clone()), base_rewards); } - logger().add_entry(LogEntry::RewardTableEntry { - node_type: node_type.clone(), - region: region.clone(), + logger.add_entry(LogEntry::RewardTableEntry { + node_type: node_type.to_string(), + region: region.to_string(), coeff, base_rewards, }); @@ -247,22 +285,22 @@ fn base_rewards_region_nodetype( let mut running_coefficient = dec!(1); let mut region_rewards = Vec::new(); - let coefficients_avg = logger().execute("Coefficients avg.", Operation::Avg(coefficients)); - let rewards_avg = logger().execute("Rewards avg.", Operation::Avg(rewards)); + let coefficients_avg = logger.execute("Coefficients avg.", Operation::Avg(coefficients)); + let rewards_avg = logger.execute("Rewards avg.", Operation::Avg(rewards)); for _ in 0..rewards_len { region_rewards.push(Operation::Multiply(rewards_avg, running_coefficient)); running_coefficient *= coefficients_avg; } - let region_rewards = logger().execute( + let region_rewards = logger.execute( "Total rewards after coefficient reduction", Operation::SumOps(region_rewards), ); - let region_rewards_avg = logger().execute( + let region_rewards_avg = logger.execute( "Rewards average after coefficient reduction", Operation::Divide(region_rewards, Decimal::from(rewards_len)), ); - logger().add_entry(LogEntry::AvgType3Rewards { + logger.add_entry(LogEntry::AvgType3Rewards { region: key.0.clone(), rewards_avg, coefficients_avg, @@ -276,16 +314,17 @@ fn base_rewards_region_nodetype( } fn node_provider_rewards( - assigned_multipliers: &AHashMap>, - rewardable_nodes: &AHashMap, + logger: &mut RewardsLog, + assigned_multipliers: &HashMap>, + rewardable_nodes: &HashMap, rewards_table: &NodeRewardsTable, ) -> Rewards { let mut rewards_xdr_total = Vec::new(); let mut rewards_xdr_no_penalty_total = Vec::new(); let rewardable_nodes_count: u32 = rewardable_nodes.values().sum(); - let region_nodetype_rewards: AHashMap = - base_rewards_region_nodetype(rewardable_nodes, rewards_table); + let region_nodetype_rewards: HashMap = + base_rewards_region_nodetype(logger, rewardable_nodes, rewards_table); // Computes the rewards multiplier for unassigned nodes as the average of the multipliers of the assigned nodes. let assigned_multipliers_v = assigned_multipliers @@ -293,11 +332,11 @@ fn node_provider_rewards( .flatten() .cloned() .collect_vec(); - let unassigned_multiplier = logger().execute( + let unassigned_multiplier = logger.execute( "Unassigned Nodes Multiplier", Operation::Avg(assigned_multipliers_v), ); - logger().add_entry(LogEntry::UnassignedMultiplier(unassigned_multiplier)); + logger.add_entry(LogEntry::UnassignedMultiplier(unassigned_multiplier)); for ((region, node_type), node_count) in rewardable_nodes { let xdr_permyriad = if node_type.starts_with("type3") { @@ -316,7 +355,7 @@ fn node_provider_rewards( // Node Providers with less than 4 machines are rewarded fully, independently of their performance if rewardable_nodes_count < FULL_REWARDS_MACHINES_LIMIT { - logger().add_entry(LogEntry::NodeCountRewardables { + logger.add_entry(LogEntry::NodeCountRewardables { node_type: node_type.clone(), region: region.clone(), count: *node_count as usize, @@ -332,7 +371,7 @@ fn node_provider_rewards( rewards_multipliers.resize(*node_count as usize, unassigned_multiplier); - logger().add_entry(LogEntry::PerformanceBasedRewardables { + logger.add_entry(LogEntry::PerformanceBasedRewardables { node_type: node_type.clone(), region: region.clone(), count: *node_count as usize, @@ -346,15 +385,15 @@ fn node_provider_rewards( } } - let rewards_xdr_total = logger().execute( + let rewards_xdr_total = logger.execute( "Compute total permyriad XDR", Operation::SumOps(rewards_xdr_total), ); - let rewards_xdr_no_reduction_total = logger().execute( + let rewards_xdr_no_reduction_total = logger.execute( "Compute total permyriad XDR no performance penalty", Operation::SumOps(rewards_xdr_no_penalty_total), ); - logger().add_entry(LogEntry::RewardsXDRTotal(rewards_xdr_total)); + logger.add_entry(LogEntry::RewardsXDRTotal(rewards_xdr_total)); Rewards { xdr_permyriad: rewards_xdr_total.to_u64().unwrap(), @@ -364,9 +403,9 @@ fn node_provider_rewards( fn node_providers_rewardables( nodes: &[RewardableNode], -) -> AHashMap { - let mut node_provider_rewardables: AHashMap = - AHashMap::default(); +) -> HashMap { + let mut node_provider_rewardables: HashMap = + HashMap::default(); nodes.iter().for_each(|node| { let (rewardable_nodes, assigned_metrics) = node_provider_rewardables @@ -386,52 +425,6 @@ fn node_providers_rewardables( node_provider_rewardables } -pub fn calculate_rewards( - days_in_period: u64, - rewards_table: &NodeRewardsTable, - rewardable_nodes: &[RewardableNode], -) -> RewardsPerNodeProvider { - let mut rewards_per_node_provider = AHashMap::default(); - let mut rewards_log_per_node_provider = AHashMap::default(); - let node_provider_rewardables = node_providers_rewardables(rewardable_nodes); - - for (node_provider_id, (rewardable_nodes, assigned_nodes_metrics)) in node_provider_rewardables - { - let mut assigned_multipliers: AHashMap> = - AHashMap::default(); - let mut nodes_multiplier_stats: Vec = Vec::new(); - let total_rewardable_nodes: u32 = rewardable_nodes.values().sum(); - - logger().add_entry(LogEntry::RewardsForNodeProvider( - node_provider_id, - total_rewardable_nodes, - )); - - for (node, daily_metrics) in assigned_nodes_metrics { - let (multiplier, multiplier_stats) = - assigned_nodes_multiplier(&daily_metrics, days_in_period); - logger().add_entry(LogEntry::RewardMultiplierForNode(node.node_id, multiplier)); - nodes_multiplier_stats.push((node.node_id, multiplier_stats)); - assigned_multipliers - .entry((node.region.clone(), node.node_type.clone())) - .or_default() - .push(multiplier); - } - - let rewards = - node_provider_rewards(&assigned_multipliers, &rewardable_nodes, rewards_table); - let node_provider_log = mem::take(&mut *logger()); - - rewards_log_per_node_provider.insert(node_provider_id, node_provider_log); - rewards_per_node_provider.insert(node_provider_id, (rewards, nodes_multiplier_stats)); - } - - RewardsPerNodeProvider { - rewards_per_node_provider, - rewards_log_per_node_provider, - } -} - #[cfg(test)] mod tests { use std::collections::BTreeMap; @@ -499,12 +492,16 @@ mod tests { #[test] fn test_rewards_percent() { + let mut logger = RewardsLog::default(); + // Overall failed = 130 Overall total = 500 Failure rate = 0.26 let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(20, 6, 4), MockedMetrics::new(25, 10, 2), ]); - let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + + let (result, _) = + assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); assert_eq!(result, dec!(0.744)); // Overall failed = 45 Overall total = 450 Failure rate = 0.1 @@ -513,37 +510,44 @@ mod tests { MockedMetrics::new(1, 400, 20), MockedMetrics::new(1, 5, 25), // no penalty ]); - let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + let (result, _) = + assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); assert_eq!(result, dec!(1.0)); // Overall failed = 5 Overall total = 10 Failure rate = 0.5 let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(1, 5, 5), // no penalty ]); - let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + let (result, _) = + assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); assert_eq!(result, dec!(0.36)); } #[test] fn test_rewards_percent_max_reduction() { + let mut logger = RewardsLog::default(); let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(10, 5, 95), // max failure rate ]); - let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + let (result, _) = + assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); assert_eq!(result, dec!(0.2)); } #[test] fn test_rewards_percent_min_reduction() { + let mut logger = RewardsLog::default(); let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(10, 9, 1), // min failure rate ]); - let (result, _) = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + let (result, _) = + assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); assert_eq!(result, dec!(1.0)); } #[test] fn test_same_rewards_percent_if_gaps_no_penalty() { + let mut logger = RewardsLog::default(); let gap = MockedMetrics::new(1, 10, 0); let daily_metrics_mid_gap: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(1, 6, 4), @@ -562,28 +566,48 @@ mod tests { ]); assert_eq!( - assigned_nodes_multiplier(&daily_metrics_mid_gap, daily_metrics_mid_gap.len() as u64).0, + assigned_nodes_multiplier( + &mut logger, + &daily_metrics_mid_gap, + daily_metrics_mid_gap.len() as u64 + ) + .0, dec!(0.7866666666666666666666666667) ); assert_eq!( - assigned_nodes_multiplier(&daily_metrics_mid_gap, daily_metrics_mid_gap.len() as u64).0, - assigned_nodes_multiplier(&daily_metrics_left_gap, daily_metrics_left_gap.len() as u64) - .0 + assigned_nodes_multiplier( + &mut logger, + &daily_metrics_mid_gap, + daily_metrics_mid_gap.len() as u64 + ) + .0, + assigned_nodes_multiplier( + &mut logger, + &daily_metrics_left_gap, + daily_metrics_left_gap.len() as u64 + ) + .0 ); assert_eq!( assigned_nodes_multiplier( + &mut logger, &daily_metrics_right_gap, daily_metrics_right_gap.len() as u64 ) .0, - assigned_nodes_multiplier(&daily_metrics_left_gap, daily_metrics_left_gap.len() as u64) - .0 + assigned_nodes_multiplier( + &mut logger, + &daily_metrics_left_gap, + daily_metrics_left_gap.len() as u64 + ) + .0 ); } #[test] fn test_same_rewards_if_reversed() { + let mut logger = RewardsLog::default(); let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(1, 5, 5), MockedMetrics::new(5, 6, 4), @@ -591,9 +615,11 @@ mod tests { ]); let mut daily_metrics = daily_metrics.clone(); - let result = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + let result = + assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); daily_metrics.reverse(); - let result_rev = assigned_nodes_multiplier(&daily_metrics, daily_metrics.len() as u64); + let result_rev = + assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); assert_eq!(result.0, dec!(1.0)); assert_eq!(result_rev.0, result.0); @@ -601,9 +627,10 @@ mod tests { #[test] fn test_np_rewards_other_type() { - let mut assigned_multipliers: AHashMap> = - AHashMap::default(); - let mut rewardable_nodes: AHashMap = AHashMap::default(); + let mut logger = RewardsLog::default(); + let mut assigned_multipliers: HashMap> = + HashMap::default(); + let mut rewardable_nodes: HashMap = HashMap::default(); let region_node_type = ("A,B,C".to_string(), "type0".to_string()); @@ -613,6 +640,7 @@ mod tests { let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); let rewards = node_provider_rewards( + &mut logger, &assigned_multipliers, &rewardable_nodes, &node_rewards_table, @@ -627,9 +655,10 @@ mod tests { #[test] fn test_np_rewards_type3_coeff() { - let mut assigned_multipliers: AHashMap> = - AHashMap::default(); - let mut rewardable_nodes: AHashMap = AHashMap::default(); + let mut logger = RewardsLog::default(); + let mut assigned_multipliers: HashMap> = + HashMap::default(); + let mut rewardable_nodes: HashMap = HashMap::default(); let region_node_type = ("A,B,C".to_string(), "type3.1".to_string()); // 4 nodes in period: 1 assigned, 3 unassigned @@ -637,6 +666,7 @@ mod tests { assigned_multipliers.insert(region_node_type, vec![dec!(0.5)]); let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); let rewards = node_provider_rewards( + &mut logger, &assigned_multipliers, &rewardable_nodes, &node_rewards_table, @@ -656,9 +686,10 @@ mod tests { #[test] fn test_np_rewards_type3_mix() { - let mut assigned_multipliers: AHashMap> = - AHashMap::default(); - let mut rewardable_nodes: AHashMap = AHashMap::default(); + let mut logger = RewardsLog::default(); + let mut assigned_multipliers: HashMap> = + HashMap::default(); + let mut rewardable_nodes: HashMap = HashMap::default(); // 5 nodes in period: 2 assigned, 3 unassigned assigned_multipliers.insert( @@ -674,6 +705,7 @@ mod tests { let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); let rewards = node_provider_rewards( + &mut logger, &assigned_multipliers, &rewardable_nodes, &node_rewards_table, diff --git a/rs/registry/node_provider_rewards/src/v1_types.rs b/rs/registry/node_provider_rewards/src/v1_types.rs index d2b7149ec15..83fd82f89e3 100644 --- a/rs/registry/node_provider_rewards/src/v1_types.rs +++ b/rs/registry/node_provider_rewards/src/v1_types.rs @@ -1,7 +1,4 @@ -use std::{ - collections::{HashMap, HashSet}, - hash::BuildHasherDefault, -}; +use std::collections::HashMap; use candid::CandidType; use ic_base_types::PrincipalId; @@ -12,13 +9,11 @@ use crate::v1_logs::RewardsLog; pub type NodeMultiplierStats = (PrincipalId, MultiplierStats); pub type RewardablesWithNodesMetrics = ( - AHashMap, - AHashMap>, + HashMap, + HashMap>, ); pub type RegionNodeTypeCategory = (String, String); pub type TimestampNanos = u64; -pub type AHashSet = HashSet>; -pub type AHashMap = HashMap>; #[derive(Clone, Hash, Eq, PartialEq)] pub struct RewardableNode { @@ -37,12 +32,12 @@ pub struct DailyNodeMetrics { pub struct NodesMetricsHistory(Vec); -impl From for AHashMap> { +impl From for HashMap> { fn from(nodes_metrics: NodesMetricsHistory) -> Self { let mut sorted_metrics = nodes_metrics.0; sorted_metrics.sort_by_key(|metrics| metrics.timestamp_nanos); - let mut sorted_metrics_per_node: AHashMap> = - AHashMap::default(); + let mut sorted_metrics_per_node: HashMap> = + HashMap::default(); for metrics in sorted_metrics { for node_metrics in metrics.node_metrics { @@ -103,8 +98,8 @@ pub struct MultiplierStats { } pub struct RewardsPerNodeProvider { - pub rewards_per_node_provider: AHashMap)>, - pub rewards_log_per_node_provider: AHashMap, + pub rewards_per_node_provider: HashMap)>, + pub rewards_log_per_node_provider: HashMap, } #[derive(Debug, Clone)] From 9fab0597a6bbe393a18b3cfaf4f99efa19315b55 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Thu, 28 Nov 2024 07:32:59 +0000 Subject: [PATCH 09/20] Automatically updated Cargo*.lock --- Cargo.Bazel.Fuzzing.json.lock | 159 ++++++++++++++++++++++++++++++---- Cargo.Bazel.Fuzzing.toml.lock | 22 +++++ Cargo.Bazel.json.lock | 159 ++++++++++++++++++++++++++++++---- 3 files changed, 310 insertions(+), 30 deletions(-) diff --git a/Cargo.Bazel.Fuzzing.json.lock b/Cargo.Bazel.Fuzzing.json.lock index 0681a9be382..ef21b388f98 100644 --- a/Cargo.Bazel.Fuzzing.json.lock +++ b/Cargo.Bazel.Fuzzing.json.lock @@ -1,5 +1,5 @@ { - "checksum": "6acc53b60ac067cb5fdd1b1316b66875d2bca3abedd239fa572c52f1620b469d", + "checksum": "9bec58b6dc593443120c2994ffd2da819710d3ff6421c17d9143ebbf413477de", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -1465,6 +1465,8 @@ ], "crate_features": { "common": [ + "compile-time-rng", + "const-random", "default", "getrandom", "runtime-rng", @@ -1482,6 +1484,10 @@ "id": "cfg-if 1.0.0", "target": "cfg_if" }, + { + "id": "const-random 0.1.18", + "target": "const_random" + }, { "id": "getrandom 0.2.10", "target": "getrandom" @@ -13703,6 +13709,110 @@ ], "license_file": "LICENSE-APACHE" }, + "const-random 0.1.18": { + "name": "const-random", + "version": "0.1.18", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random/0.1.18/download", + "sha256": "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" + } + }, + "targets": [ + { + "Library": { + "crate_name": "const_random", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "const-random-macro 0.1.16", + "target": "const_random_macro" + } + ], + "selects": {} + }, + "version": "0.1.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "const-random-macro 0.1.16": { + "name": "const-random-macro", + "version": "0.1.16", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random-macro/0.1.16/download", + "sha256": "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "const_random_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "getrandom 0.2.10", + "target": "getrandom" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "tiny-keccak 2.0.2", + "target": "tiny_keccak" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.16" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "convert_case 0.4.0": { "name": "convert_case", "version": "0.4.0", @@ -18419,6 +18529,10 @@ "id": "addr 0.15.6", "target": "addr" }, + { + "id": "ahash 0.8.11", + "target": "ahash" + }, { "id": "aide 0.13.4", "target": "aide" @@ -72494,46 +72608,60 @@ ], "selects": { "aarch64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "aarch64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ], "arm-unknown-linux-gnueabi": [ - "sha3" + "sha3", + "shake" ], "i686-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "i686-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "powerpc-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "s390x-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "x86_64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-freebsd": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ] } }, @@ -87085,6 +87213,7 @@ "actix-rt 2.10.0", "actix-web 4.9.0", "addr 0.15.6", + "ahash 0.8.11", "aide 0.13.4", "anyhow 1.0.93", "arbitrary 1.3.2", diff --git a/Cargo.Bazel.Fuzzing.toml.lock b/Cargo.Bazel.Fuzzing.toml.lock index 283bc20d2c3..4e1593fb7b4 100644 --- a/Cargo.Bazel.Fuzzing.toml.lock +++ b/Cargo.Bazel.Fuzzing.toml.lock @@ -275,6 +275,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if 1.0.0", + "const-random", "getrandom", "once_cell", "version_check", @@ -2253,6 +2254,26 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom", + "once_cell", + "tiny-keccak", +] + [[package]] name = "convert_case" version = "0.4.0" @@ -2983,6 +3004,7 @@ dependencies = [ "actix-rt", "actix-web", "addr", + "ahash 0.8.11", "aide", "anyhow", "arbitrary", diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index dbe6fe517c2..1cdd3f02cbe 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "636169f370831d101e74dcb54f78c8130c106a244347ce50b4b34b43589709e3", + "checksum": "7bb477f998ed4a67dc53d2aee87201e4e2ea4af0e3592c13941ff3e520010c76", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -1469,6 +1469,8 @@ ], "crate_features": { "common": [ + "compile-time-rng", + "const-random", "default", "getrandom", "runtime-rng", @@ -1486,6 +1488,10 @@ "id": "cfg-if 1.0.0", "target": "cfg_if" }, + { + "id": "const-random 0.1.18", + "target": "const_random" + }, { "id": "getrandom 0.2.10", "target": "getrandom" @@ -13531,6 +13537,110 @@ ], "license_file": "LICENSE-APACHE" }, + "const-random 0.1.18": { + "name": "const-random", + "version": "0.1.18", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random/0.1.18/download", + "sha256": "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" + } + }, + "targets": [ + { + "Library": { + "crate_name": "const_random", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "const-random-macro 0.1.16", + "target": "const_random_macro" + } + ], + "selects": {} + }, + "version": "0.1.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "const-random-macro 0.1.16": { + "name": "const-random-macro", + "version": "0.1.16", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random-macro/0.1.16/download", + "sha256": "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "const_random_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "getrandom 0.2.10", + "target": "getrandom" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "tiny-keccak 2.0.2", + "target": "tiny_keccak" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.16" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "convert_case 0.4.0": { "name": "convert_case", "version": "0.4.0", @@ -18247,6 +18357,10 @@ "id": "addr 0.15.6", "target": "addr" }, + { + "id": "ahash 0.8.11", + "target": "ahash" + }, { "id": "aide 0.13.4", "target": "aide" @@ -72340,46 +72454,60 @@ ], "selects": { "aarch64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "aarch64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ], "arm-unknown-linux-gnueabi": [ - "sha3" + "sha3", + "shake" ], "i686-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "i686-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "powerpc-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "s390x-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "x86_64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-freebsd": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ] } }, @@ -86965,6 +87093,7 @@ "actix-rt 2.10.0", "actix-web 4.9.0", "addr 0.15.6", + "ahash 0.8.11", "aide 0.13.4", "anyhow 1.0.93", "arbitrary 1.3.2", From 3e406a6b8edd9dff62a31273b2906647569fcd90 Mon Sep 17 00:00:00 2001 From: Pietro Date: Mon, 9 Dec 2024 19:22:23 +0100 Subject: [PATCH 10/20] Add systemic FR from subnet --- .../node_provider_rewards/src/v1_rewards.rs | 769 ++++++------------ .../src/v1_rewards/tests.rs | 672 +++++++++++++++ .../node_provider_rewards/src/v1_types.rs | 89 +- 3 files changed, 967 insertions(+), 563 deletions(-) create mode 100644 rs/registry/node_provider_rewards/src/v1_rewards/tests.rs diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index 1b648e834d7..dafb77382fa 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -3,13 +3,14 @@ use ic_protobuf::registry::node_rewards::v2::{NodeRewardRate, NodeRewardsTable}; use itertools::Itertools; use num_traits::ToPrimitive; +use crate::v1_types::TimestampNanos; use crate::{ v1_logs::{LogEntry, Operation, RewardsLog}, v1_types::{ - DailyNodeMetrics, MultiplierStats, NodeMultiplierStats, RegionNodeTypeCategory, - RewardableNode, RewardablesWithNodesMetrics, Rewards, RewardsPerNodeProvider, + DailyNodeMetrics, RegionNodeTypeCategory, RewardableNode, Rewards, RewardsPerNodeProvider, }, }; +use ic_management_canister_types::{NodeMetrics, NodeMetricsHistoryResponse}; use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::collections::HashMap; @@ -19,48 +20,51 @@ const MIN_FAILURE_RATE: Decimal = dec!(0.1); const MAX_FAILURE_RATE: Decimal = dec!(0.6); const RF: &str = "Linear Reduction factor"; +// The algo works as follows: +// 1. compute systematics failure rate of the subnets for each day currently 75percentile +// 2. derive idiosyncratic_daily_fr for each node in the subnet +// 3. compute the avg idiosyncratic failure rate to be assigned to each active period of the node +// 4. compute the avg idiosyncratic failure rates across all nodes for the node provider to be used in unassigned periods +// 5. for each node compute rewards multiplier +// 6. multiply per base rewards pub fn calculate_rewards( days_in_period: u64, rewards_table: &NodeRewardsTable, + subnet_metrics: HashMap>, rewardable_nodes: &[RewardableNode], ) -> RewardsPerNodeProvider { let mut rewards_per_node_provider = HashMap::default(); let mut rewards_log_per_node_provider = HashMap::default(); - let node_provider_rewardables = node_providers_rewardables(rewardable_nodes); + let mut all_assigned_metrics = daily_node_metrics(subnet_metrics); - for (node_provider_id, (rewardable_nodes, assigned_nodes_metrics)) in node_provider_rewardables - { + let subnets_systematic_fr = systematic_fr_per_subnet(&all_assigned_metrics); + let node_provider_rewardables = rewardables_by_node_provider(rewardable_nodes); + + for (node_provider_id, node_provider_rewardables) in node_provider_rewardables { let mut logger = RewardsLog::default(); - let mut assigned_multipliers: HashMap> = - HashMap::default(); - let mut nodes_multiplier_stats: Vec = Vec::new(); - let total_rewardable_nodes: u32 = rewardable_nodes.values().sum(); - - logger.add_entry(LogEntry::RewardsForNodeProvider( - node_provider_id, - total_rewardable_nodes, - )); - - for (node, daily_metrics) in assigned_nodes_metrics { - let (multiplier, multiplier_stats) = - assigned_nodes_multiplier(&mut logger, &daily_metrics, days_in_period); - logger.add_entry(LogEntry::RewardMultiplierForNode(node.node_id, multiplier)); - nodes_multiplier_stats.push((node.node_id, multiplier_stats)); - assigned_multipliers - .entry((node.region.clone(), node.node_type.clone())) - .or_default() - .push(multiplier); - } + + let assigned_metrics: HashMap> = + node_provider_rewardables + .iter() + .filter_map(|node| { + all_assigned_metrics + .remove(&node.node_id) + .map(|daily_metrics| (node.node_id, daily_metrics)) + }) + .collect::>>(); + let idiosyncratic_daily_fr = + idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); let rewards = node_provider_rewards( &mut logger, - &assigned_multipliers, - &rewardable_nodes, + &node_provider_rewardables, + idiosyncratic_daily_fr, + days_in_period, rewards_table, ); rewards_log_per_node_provider.insert(node_provider_id, logger); - rewards_per_node_provider.insert(node_provider_id, (rewards, nodes_multiplier_stats)); + rewards_per_node_provider.insert(node_provider_id, rewards); } RewardsPerNodeProvider { @@ -69,95 +73,245 @@ pub fn calculate_rewards( } } -/// Assigned nodes multiplier -/// -/// Computes the rewards multiplier for a single assigned node based on the overall failure rate in the period. -/// -/// 1. The function iterates through each day's metrics, summing up the `daily_failed` and `daily_total` blocks across all days. -/// 2. The `overall_failure_rate` for the period is calculated by dividing the `overall_failed` blocks by the `overall_total` blocks. -/// 3. The `rewards_reduction` function is applied to `overall_failure_rate`. -/// 3. Finally, the rewards multiplier to be distributed to the node is computed. -pub fn assigned_nodes_multiplier( +fn idiosyncratic_daily_fr( + assigned_metrics: &HashMap>, + subnets_systematic_fr: &HashMap<(PrincipalId, TimestampNanos), Decimal>, +) -> HashMap> { + let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); + + for (node_id, daily_metrics) in assigned_metrics { + for metrics in daily_metrics { + let systematic_fr = subnets_systematic_fr + .get(&(metrics.subnet_assigned, metrics.ts)) + .expect("Systematic failure rate not found"); + let failure_rates = nodes_idiosyncratic_fr.entry(*node_id).or_default(); + let fr = if metrics.failure_rate < *systematic_fr { + Decimal::ZERO + } else { + metrics.failure_rate - *systematic_fr + }; + failure_rates.push(fr); + } + } + + nodes_idiosyncratic_fr +} + +fn node_provider_rewards( logger: &mut RewardsLog, - daily_metrics: &[DailyNodeMetrics], - total_days: u64, -) -> (Decimal, MultiplierStats) { - let total_days = Decimal::from(total_days); - - let days_assigned = logger.execute( - "Assigned Days In Period", - Operation::Set(Decimal::from(daily_metrics.len())), - ); - let days_unassigned = logger.execute( - "Unassigned Days In Period", - Operation::Subtract(total_days, days_assigned), - ); + rewardables: &[RewardableNode], + nodes_idiosyncratic_fr: HashMap>, + days_in_period: u64, + rewards_table: &NodeRewardsTable, +) -> Rewards { + let mut rewards_xdr_total = Vec::new(); + let mut rewards_xdr_no_penalty_total = Vec::new(); - let daily_failed = daily_metrics - .iter() - .map(|metrics| metrics.num_blocks_failed.into()) - .collect_vec(); - let daily_proposed = daily_metrics - .iter() - .map(|metrics| metrics.num_blocks_proposed.into()) - .collect_vec(); + let mut avg_assigned_fr: Vec = Vec::new(); + let mut region_node_type_rewardables = HashMap::new(); - let overall_failed = logger.execute( - "Computing Total Failed Blocks", - Operation::Sum(daily_failed), - ); - let overall_proposed = logger.execute( - "Computing Total Proposed Blocks", - Operation::Sum(daily_proposed), - ); - let overall_total = logger.execute( - "Computing Total Blocks", - Operation::Sum(vec![overall_failed, overall_proposed]), + let rewardable_nodes_count = rewardables.len() as u32; + let mut nodes_idiosyncratic_fr = nodes_idiosyncratic_fr; + + for node in rewardables { + // Count the number of nodes per region and node type + let nodes_count = region_node_type_rewardables + .entry((node.region.clone(), node.node_type.clone())) + .or_default(); + *nodes_count += 1; + + // 1. Compute the unassigned failure rate + if let Some(daily_fr) = nodes_idiosyncratic_fr.get(&node.node_id) { + let assigned_fr = daily_fr.iter().sum::() / Decimal::from(daily_fr.len()); + println!("assigned_fr: {}", assigned_fr); + avg_assigned_fr.push(assigned_fr); + } + } + + let region_nodetype_rewards: HashMap = + base_rewards_region_nodetype(logger, ®ion_node_type_rewardables, rewards_table); + + let avg_assigned_fr_len = avg_assigned_fr.len(); + let unassigned_fr: Decimal = if avg_assigned_fr_len > 0 { + avg_assigned_fr.iter().sum::() / Decimal::from(avg_assigned_fr.len()) + } else { + dec!(1) + }; + + println!("unassigned_fr: {}", unassigned_fr); + + let rewards_reduction_unassigned = rewards_reduction_percent(logger, &unassigned_fr); + let multiplier_unassigned = logger.execute( + "Reward Multiplier Fully Unassigned Nodes", + Operation::Subtract(dec!(1), rewards_reduction_unassigned), ); - let overall_failure_rate = logger.execute( - "Computing Total Failure Rate", - if overall_total > dec!(0) { - Operation::Divide(overall_failed, overall_total) + + println!("multiplier_unassigned: {}", multiplier_unassigned); + + // 3. reward the nodes of node provider + let mut sorted_rewardables = rewardables.to_vec(); + sorted_rewardables.sort_by(|a, b| a.region.cmp(&b.region).then(a.node_type.cmp(&b.node_type))); + for node in sorted_rewardables { + let node_type = node.node_type.clone(); + let region = node.region.clone(); + + let rewards_xdr_no_penalty = if node_type.starts_with("type3") { + let region_key = region_type3_key(region.clone()); + region_nodetype_rewards + .get(®ion_key) + .expect("Type3 rewards already filled") } else { - Operation::Set(dec!(0)) - }, - ); + region_nodetype_rewards + .get(&(node.region.clone(), node.node_type.clone())) + .expect("Rewards already filled") + }; - let rewards_reduction = rewards_reduction_percent(logger, &overall_failure_rate); - let rewards_multiplier_assigned = logger.execute( - "Reward Multiplier Assigned Days", - Operation::Subtract(dec!(1), rewards_reduction), - ); + rewards_xdr_no_penalty_total.push(Operation::Multiply(*rewards_xdr_no_penalty, dec!(1))); - // On days when the node is not assigned to a subnet, it will receive the same `Reward Multiplier` as computed for the days it was assigned. - let rewards_multiplier_unassigned = logger.execute( - "Reward Multiplier Unassigned Days", - Operation::Set(rewards_multiplier_assigned), - ); - let assigned_days_factor = logger.execute( - "Assigned Days Factor", - Operation::Multiply(days_assigned, rewards_multiplier_assigned), - ); - let unassigned_days_factor = logger.execute( - "Unassigned Days Factor (currently equal to Assigned Days Factor)", - Operation::Multiply(days_unassigned, rewards_multiplier_unassigned), + // Node Providers with less than 4 machines are rewarded fully, independently of their performance + if rewardable_nodes_count < FULL_REWARDS_MACHINES_LIMIT { + rewards_xdr_total.push(Operation::Multiply(*rewards_xdr_no_penalty, dec!(1))); + continue; + } + + // In this case the node has been assigned to a subnet + if let Some(mut daily_idiosyncratic_fr) = nodes_idiosyncratic_fr.remove(&node.node_id) { + // resize the daily_idiosyncratic_fr to the number of days in the period + daily_idiosyncratic_fr.resize(days_in_period as usize, unassigned_fr); + + let multiplier_assigned = assigned_multiplier(logger, daily_idiosyncratic_fr); + + println!("rewards_xdr_no_penalty: {}", rewards_xdr_no_penalty); + println!("multiplier_assigned: {}", multiplier_assigned); + + rewards_xdr_total.push(Operation::Multiply( + *rewards_xdr_no_penalty, + multiplier_assigned, + )); + } else { + rewards_xdr_total.push(Operation::Multiply( + *rewards_xdr_no_penalty, + multiplier_unassigned, + )); + } + } + + let rewards_xdr_total = logger.execute( + "Compute total permyriad XDR", + Operation::SumOps(rewards_xdr_total), ); - let rewards_multiplier = logger.execute( - "Average reward multiplier", - Operation::Divide(assigned_days_factor + unassigned_days_factor, total_days), + let rewards_xdr_no_reduction_total = logger.execute( + "Compute total permyriad XDR no performance penalty", + Operation::SumOps(rewards_xdr_no_penalty_total), ); + logger.add_entry(LogEntry::RewardsXDRTotal(rewards_xdr_total)); - let rewards_multiplier_stats = MultiplierStats { - days_assigned: days_assigned.to_u64().unwrap(), - days_unassigned: days_unassigned.to_u64().unwrap(), - rewards_reduction: rewards_reduction.to_f64().unwrap(), - blocks_failed: overall_failed.to_u64().unwrap(), - blocks_proposed: overall_proposed.to_u64().unwrap(), - blocks_total: overall_total.to_u64().unwrap(), - failure_rate: overall_failure_rate.to_f64().unwrap(), - }; + Rewards { + xdr_permyriad: rewards_xdr_total.to_u64().unwrap(), + xdr_permyriad_no_reduction: rewards_xdr_no_reduction_total.to_u64().unwrap(), + } +} + +fn assigned_multiplier(logger: &mut RewardsLog, daily_failure_rate: Vec) -> Decimal { + let average_fr = logger.execute("Failure rate average", Operation::Avg(daily_failure_rate)); + let rewards_reduction = rewards_reduction_percent(logger, &average_fr); - (rewards_multiplier, rewards_multiplier_stats) + logger.execute( + "Reward Multiplier Assigned", + Operation::Subtract(dec!(1), rewards_reduction), + ) +} + +fn systematic_fr_per_subnet( + daily_node_metrics: &HashMap>, +) -> HashMap<(PrincipalId, TimestampNanos), Decimal> { + fn percentile_75(mut values: Vec) -> Decimal { + values.sort(); + let len = values.len(); + if len == 0 { + return Decimal::ZERO; + } + let idx = ((len as f64) * 0.75).ceil() as usize - 1; + values[idx] + } + + let mut subnet_daily_failure_rates: HashMap<(PrincipalId, u64), Vec> = HashMap::new(); + + for (_, metrics) in daily_node_metrics { + for metric in metrics { + subnet_daily_failure_rates + .entry((metric.subnet_assigned.clone(), metric.ts)) + .or_default() + .push(metric.failure_rate); + } + } + + subnet_daily_failure_rates + .into_iter() + .map(|((subnet, ts), failure_rates)| ((subnet, ts), percentile_75(failure_rates))) + .collect() +} + +fn daily_node_metrics( + subnets_metrics: HashMap>, +) -> HashMap> { + let mut subnets_metrics = subnets_metrics + .into_iter() + .flat_map(|(subnet_id, metrics)| { + metrics.into_iter().map(move |metrics| (subnet_id, metrics)) + }) + .collect_vec(); + subnets_metrics.sort_by_key(|(_, metrics)| metrics.timestamp_nanos); + + let mut daily_node_metrics: HashMap> = + HashMap::default(); + + for (subnet_id, metrics) in subnets_metrics { + for node_metrics in metrics.node_metrics { + daily_node_metrics + .entry(node_metrics.node_id) + .or_default() + .push((subnet_id, metrics.timestamp_nanos, node_metrics)); + } + } + + daily_node_metrics + .into_iter() + .map(|(node_id, metrics)| { + let mut daily_metrics = Vec::new(); + let mut previous_proposed_total = 0; + let mut previous_failed_total = 0; + + for (subnet_id, ts, node_metrics) in metrics { + let current_proposed_total = node_metrics.num_blocks_proposed_total; + let current_failed_total = node_metrics.num_block_failures_total; + + let (num_blocks_proposed, num_blocks_failed) = if previous_failed_total + > current_failed_total + || previous_proposed_total > current_proposed_total + { + // This is the case when node is deployed again + (current_proposed_total, current_failed_total) + } else { + ( + current_proposed_total - previous_proposed_total, + current_failed_total - previous_failed_total, + ) + }; + + daily_metrics.push(DailyNodeMetrics::new( + ts, + subnet_id, + num_blocks_proposed, + num_blocks_failed, + )); + + previous_proposed_total = current_proposed_total; + previous_failed_total = current_failed_total; + } + (node_id, daily_metrics) + }) + .collect() } /// Calculates the rewards reduction based on the failure rate. @@ -313,412 +467,21 @@ fn base_rewards_region_nodetype( region_nodetype_rewards } -fn node_provider_rewards( - logger: &mut RewardsLog, - assigned_multipliers: &HashMap>, - rewardable_nodes: &HashMap, - rewards_table: &NodeRewardsTable, -) -> Rewards { - let mut rewards_xdr_total = Vec::new(); - let mut rewards_xdr_no_penalty_total = Vec::new(); - let rewardable_nodes_count: u32 = rewardable_nodes.values().sum(); - - let region_nodetype_rewards: HashMap = - base_rewards_region_nodetype(logger, rewardable_nodes, rewards_table); - - // Computes the rewards multiplier for unassigned nodes as the average of the multipliers of the assigned nodes. - let assigned_multipliers_v = assigned_multipliers - .values() - .flatten() - .cloned() - .collect_vec(); - let unassigned_multiplier = logger.execute( - "Unassigned Nodes Multiplier", - Operation::Avg(assigned_multipliers_v), - ); - logger.add_entry(LogEntry::UnassignedMultiplier(unassigned_multiplier)); - - for ((region, node_type), node_count) in rewardable_nodes { - let xdr_permyriad = if node_type.starts_with("type3") { - let region_key = region_type3_key(region.clone()); - region_nodetype_rewards - .get(®ion_key) - .expect("Type3 rewards already filled") - } else { - region_nodetype_rewards - .get(&(region.clone(), node_type.clone())) - .expect("Rewards already filled") - }; - let rewards_xdr_no_penalty = - Operation::Multiply(*xdr_permyriad, Decimal::from(*node_count)); - rewards_xdr_no_penalty_total.push(rewards_xdr_no_penalty.clone()); - - // Node Providers with less than 4 machines are rewarded fully, independently of their performance - if rewardable_nodes_count < FULL_REWARDS_MACHINES_LIMIT { - logger.add_entry(LogEntry::NodeCountRewardables { - node_type: node_type.clone(), - region: region.clone(), - count: *node_count as usize, - }); - - rewards_xdr_total.push(rewards_xdr_no_penalty); - } else { - let mut rewards_multipliers = assigned_multipliers - .get(&(region.clone(), node_type.clone())) - .cloned() - .unwrap_or_default(); - let assigned_len = rewards_multipliers.len(); - - rewards_multipliers.resize(*node_count as usize, unassigned_multiplier); - - logger.add_entry(LogEntry::PerformanceBasedRewardables { - node_type: node_type.clone(), - region: region.clone(), - count: *node_count as usize, - assigned_multipliers: rewards_multipliers[..assigned_len].to_vec(), - unassigned_multipliers: rewards_multipliers[assigned_len..].to_vec(), - }); - - for multiplier in rewards_multipliers { - rewards_xdr_total.push(Operation::Multiply(*xdr_permyriad, multiplier)); - } - } - } - - let rewards_xdr_total = logger.execute( - "Compute total permyriad XDR", - Operation::SumOps(rewards_xdr_total), - ); - let rewards_xdr_no_reduction_total = logger.execute( - "Compute total permyriad XDR no performance penalty", - Operation::SumOps(rewards_xdr_no_penalty_total), - ); - logger.add_entry(LogEntry::RewardsXDRTotal(rewards_xdr_total)); - - Rewards { - xdr_permyriad: rewards_xdr_total.to_u64().unwrap(), - xdr_permyriad_no_reduction: rewards_xdr_no_reduction_total.to_u64().unwrap(), - } -} - -fn node_providers_rewardables( +fn rewardables_by_node_provider( nodes: &[RewardableNode], -) -> HashMap { - let mut node_provider_rewardables: HashMap = +) -> HashMap> { + let mut node_provider_rewardables: HashMap> = HashMap::default(); nodes.iter().for_each(|node| { - let (rewardable_nodes, assigned_metrics) = node_provider_rewardables + let rewardable_nodes = node_provider_rewardables .entry(node.node_provider_id) .or_default(); - - let nodes_count = rewardable_nodes - .entry((node.region.clone(), node.node_type.clone())) - .or_default(); - *nodes_count += 1; - - if let Some(daily_metrics) = &node.node_metrics { - assigned_metrics.insert(node.clone(), daily_metrics.clone()); - } + rewardable_nodes.push(node.clone()); }); node_provider_rewardables } #[cfg(test)] -mod tests { - use std::collections::BTreeMap; - - use ic_protobuf::registry::node_rewards::v2::NodeRewardRates; - use itertools::Itertools; - - use super::*; - - #[derive(Clone)] - struct MockedMetrics { - days: u64, - proposed_blocks: u64, - failed_blocks: u64, - } - - impl MockedMetrics { - fn new(days: u64, proposed_blocks: u64, failed_blocks: u64) -> Self { - MockedMetrics { - days, - proposed_blocks, - failed_blocks, - } - } - } - - fn daily_mocked_metrics(metrics: Vec) -> Vec { - metrics - .into_iter() - .flat_map(|mocked_metrics: MockedMetrics| { - (0..mocked_metrics.days).map(move |_| DailyNodeMetrics { - num_blocks_proposed: mocked_metrics.proposed_blocks, - num_blocks_failed: mocked_metrics.failed_blocks, - }) - }) - .collect_vec() - } - - fn mocked_rewards_table() -> NodeRewardsTable { - let mut rates_outer: BTreeMap = BTreeMap::new(); - let mut rates_inner: BTreeMap = BTreeMap::new(); - let mut table: BTreeMap = BTreeMap::new(); - - let rate_outer = NodeRewardRate { - xdr_permyriad_per_node_per_month: 1000, - reward_coefficient_percent: Some(97), - }; - - let rate_inner = NodeRewardRate { - xdr_permyriad_per_node_per_month: 1500, - reward_coefficient_percent: Some(95), - }; - - rates_outer.insert("type0".to_string(), rate_outer); - rates_outer.insert("type1".to_string(), rate_outer); - rates_outer.insert("type3".to_string(), rate_outer); - - rates_inner.insert("type3.1".to_string(), rate_inner); - - table.insert("A,B,C".to_string(), NodeRewardRates { rates: rates_inner }); - table.insert("A,B".to_string(), NodeRewardRates { rates: rates_outer }); - - NodeRewardsTable { table } - } - - #[test] - fn test_rewards_percent() { - let mut logger = RewardsLog::default(); - - // Overall failed = 130 Overall total = 500 Failure rate = 0.26 - let daily_metrics: Vec = daily_mocked_metrics(vec![ - MockedMetrics::new(20, 6, 4), - MockedMetrics::new(25, 10, 2), - ]); - - let (result, _) = - assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); - assert_eq!(result, dec!(0.744)); - - // Overall failed = 45 Overall total = 450 Failure rate = 0.1 - // rewards_reduction = 0.0 - let daily_metrics: Vec = daily_mocked_metrics(vec![ - MockedMetrics::new(1, 400, 20), - MockedMetrics::new(1, 5, 25), // no penalty - ]); - let (result, _) = - assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); - assert_eq!(result, dec!(1.0)); - - // Overall failed = 5 Overall total = 10 Failure rate = 0.5 - let daily_metrics: Vec = daily_mocked_metrics(vec![ - MockedMetrics::new(1, 5, 5), // no penalty - ]); - let (result, _) = - assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); - assert_eq!(result, dec!(0.36)); - } - - #[test] - fn test_rewards_percent_max_reduction() { - let mut logger = RewardsLog::default(); - let daily_metrics: Vec = daily_mocked_metrics(vec![ - MockedMetrics::new(10, 5, 95), // max failure rate - ]); - let (result, _) = - assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); - assert_eq!(result, dec!(0.2)); - } - - #[test] - fn test_rewards_percent_min_reduction() { - let mut logger = RewardsLog::default(); - let daily_metrics: Vec = daily_mocked_metrics(vec![ - MockedMetrics::new(10, 9, 1), // min failure rate - ]); - let (result, _) = - assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); - assert_eq!(result, dec!(1.0)); - } - - #[test] - fn test_same_rewards_percent_if_gaps_no_penalty() { - let mut logger = RewardsLog::default(); - let gap = MockedMetrics::new(1, 10, 0); - let daily_metrics_mid_gap: Vec = daily_mocked_metrics(vec![ - MockedMetrics::new(1, 6, 4), - gap.clone(), - MockedMetrics::new(1, 7, 3), - ]); - let daily_metrics_left_gap: Vec = daily_mocked_metrics(vec![ - gap.clone(), - MockedMetrics::new(1, 6, 4), - MockedMetrics::new(1, 7, 3), - ]); - let daily_metrics_right_gap: Vec = daily_mocked_metrics(vec![ - gap.clone(), - MockedMetrics::new(1, 6, 4), - MockedMetrics::new(1, 7, 3), - ]); - - assert_eq!( - assigned_nodes_multiplier( - &mut logger, - &daily_metrics_mid_gap, - daily_metrics_mid_gap.len() as u64 - ) - .0, - dec!(0.7866666666666666666666666667) - ); - - assert_eq!( - assigned_nodes_multiplier( - &mut logger, - &daily_metrics_mid_gap, - daily_metrics_mid_gap.len() as u64 - ) - .0, - assigned_nodes_multiplier( - &mut logger, - &daily_metrics_left_gap, - daily_metrics_left_gap.len() as u64 - ) - .0 - ); - assert_eq!( - assigned_nodes_multiplier( - &mut logger, - &daily_metrics_right_gap, - daily_metrics_right_gap.len() as u64 - ) - .0, - assigned_nodes_multiplier( - &mut logger, - &daily_metrics_left_gap, - daily_metrics_left_gap.len() as u64 - ) - .0 - ); - } - - #[test] - fn test_same_rewards_if_reversed() { - let mut logger = RewardsLog::default(); - let daily_metrics: Vec = daily_mocked_metrics(vec![ - MockedMetrics::new(1, 5, 5), - MockedMetrics::new(5, 6, 4), - MockedMetrics::new(25, 10, 0), - ]); - - let mut daily_metrics = daily_metrics.clone(); - let result = - assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); - daily_metrics.reverse(); - let result_rev = - assigned_nodes_multiplier(&mut logger, &daily_metrics, daily_metrics.len() as u64); - - assert_eq!(result.0, dec!(1.0)); - assert_eq!(result_rev.0, result.0); - } - - #[test] - fn test_np_rewards_other_type() { - let mut logger = RewardsLog::default(); - let mut assigned_multipliers: HashMap> = - HashMap::default(); - let mut rewardable_nodes: HashMap = HashMap::default(); - - let region_node_type = ("A,B,C".to_string(), "type0".to_string()); - - // 4 nodes in period: 2 assigned, 2 unassigned - rewardable_nodes.insert(region_node_type.clone(), 4); - assigned_multipliers.insert(region_node_type.clone(), vec![dec!(0.5), dec!(0.5)]); - - let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); - let rewards = node_provider_rewards( - &mut logger, - &assigned_multipliers, - &rewardable_nodes, - &node_rewards_table, - ); - - // Total XDR no penalties, operation=sum(1000,1000,1000,1000), result=4000 - assert_eq!(rewards.xdr_permyriad_no_reduction, 4000); - - // Total XDR, operation=sum(1000 * 0.5,1000 * 0.5,1000 * 0.5,1000 * 0.5), result=2000 - assert_eq!(rewards.xdr_permyriad, 2000); - } - - #[test] - fn test_np_rewards_type3_coeff() { - let mut logger = RewardsLog::default(); - let mut assigned_multipliers: HashMap> = - HashMap::default(); - let mut rewardable_nodes: HashMap = HashMap::default(); - let region_node_type = ("A,B,C".to_string(), "type3.1".to_string()); - - // 4 nodes in period: 1 assigned, 3 unassigned - rewardable_nodes.insert(region_node_type.clone(), 4); - assigned_multipliers.insert(region_node_type, vec![dec!(0.5)]); - let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); - let rewards = node_provider_rewards( - &mut logger, - &assigned_multipliers, - &rewardable_nodes, - &node_rewards_table, - ); - - // Coefficients avg., operation=avg(0.95,0.95,0.95,0.95), result=0.95 - // Rewards avg., operation=avg(1500,1500,1500,1500), result=1500 - // Total rewards after coefficient reduction, operation=sum(1500 * 1,1500 * 0.95,1500 * 0.9025,1500 * 0.8574), result=5564 - - // Rewards average after coefficient reduction, operation=5564 / 4, result=1391 - // Total XDR no penalties, operation=sum(1391,1391,1391,1391), result=5564 - assert_eq!(rewards.xdr_permyriad_no_reduction, 5564); - - // Total XDR, operation=sum(1391 * 0.5,1391 * 0.5,1391 * 0.5,1391 * 0.5), result=2782 - assert_eq!(rewards.xdr_permyriad, 2782); - } - - #[test] - fn test_np_rewards_type3_mix() { - let mut logger = RewardsLog::default(); - let mut assigned_multipliers: HashMap> = - HashMap::default(); - let mut rewardable_nodes: HashMap = HashMap::default(); - - // 5 nodes in period: 2 assigned, 3 unassigned - assigned_multipliers.insert( - ("A,B,D".to_string(), "type3".to_string()), - vec![dec!(0.5), dec!(0.4)], - ); - - // This will take rates from outer - rewardable_nodes.insert(("A,B,D".to_string(), "type3".to_string()), 3); - - // This will take rates from inner - rewardable_nodes.insert(("A,B,C".to_string(), "type3.1".to_string()), 2); - - let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); - let rewards = node_provider_rewards( - &mut logger, - &assigned_multipliers, - &rewardable_nodes, - &node_rewards_table, - ); - - // Coefficients avg(0.95,0.95,0.97,0.97,0.97) = 0.9620 - // Rewards avg., operation=avg(1500,1500,1000,1000,1000), result=1200 - // Rewards average sum(1200 * 1,1200 * 0.9620,1200 * 0.9254,1200 * 0.8903,1200 * 0.8564) / 5, result=1112 - // Unassigned Nodes Multiplier, operation=avg(0.5,0.4), result=0.450 - - // Total XDR, operation=sum(1112 * 0.450,1112 * 0.450,1112 * 0.5,1112 * 0.4,1112 * 0.450), result=2502 - assert_eq!(rewards.xdr_permyriad, 2502); - // Total XDR no penalties, operation=1112 * 5, result=5561 - assert_eq!(rewards.xdr_permyriad_no_reduction, 5561); - } -} +mod tests; diff --git a/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs new file mode 100644 index 00000000000..201019cc176 --- /dev/null +++ b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs @@ -0,0 +1,672 @@ +use ic_protobuf::registry::node_rewards::v2::NodeRewardRates; +use num_traits::FromPrimitive; +use std::collections::BTreeMap; + +use super::*; + +#[derive(Clone)] +struct MockedMetrics { + days: u64, + proposed_blocks: u64, + failed_blocks: u64, +} + +impl MockedMetrics { + fn new(days: u64, proposed_blocks: u64, failed_blocks: u64) -> Self { + MockedMetrics { + days, + proposed_blocks, + failed_blocks, + } + } +} + +impl DailyNodeMetrics { + fn from_fr_dummy(ts: u64, subnet_assigned: PrincipalId, failure_rate: Decimal) -> Self { + let num_blocks_proposed = 10; + let num_blocks_failed = if failure_rate.is_zero() { + 0 + } else { + let total_blocks = + (failure_rate / (Decimal::ONE - failure_rate)) * Decimal::from(num_blocks_proposed); + total_blocks.floor().to_u64().unwrap_or(0) + }; + DailyNodeMetrics { + ts, + subnet_assigned, + num_blocks_proposed, + num_blocks_failed, + failure_rate, + } + } +} + +fn daily_mocked_failure_rates(metrics: Vec) -> Vec { + metrics + .into_iter() + .flat_map(|mocked_metrics: MockedMetrics| { + (0..mocked_metrics.days).map(move |i| { + DailyNodeMetrics::new( + i, + PrincipalId::new_anonymous(), + mocked_metrics.proposed_blocks, + mocked_metrics.failed_blocks, + ) + .failure_rate + }) + }) + .collect() +} +fn mocked_rewards_table() -> NodeRewardsTable { + let mut rates_outer: BTreeMap = BTreeMap::new(); + let mut rates_inner: BTreeMap = BTreeMap::new(); + let mut table: BTreeMap = BTreeMap::new(); + + let rate_outer = NodeRewardRate { + xdr_permyriad_per_node_per_month: 1000, + reward_coefficient_percent: Some(97), + }; + + let rate_inner = NodeRewardRate { + xdr_permyriad_per_node_per_month: 1500, + reward_coefficient_percent: Some(95), + }; + + rates_outer.insert("type0".to_string(), rate_outer); + rates_outer.insert("type1".to_string(), rate_outer); + rates_outer.insert("type3".to_string(), rate_outer); + + rates_inner.insert("type3.1".to_string(), rate_inner); + + table.insert("A,B,C".to_string(), NodeRewardRates { rates: rates_inner }); + table.insert("A,B".to_string(), NodeRewardRates { rates: rates_outer }); + + NodeRewardsTable { table } +} +#[test] +fn test_daily_node_metrics() { + let subnet1 = PrincipalId::new_user_test_id(1); + let subnet2 = PrincipalId::new_user_test_id(2); + + let node1 = PrincipalId::new_user_test_id(101); + let node2 = PrincipalId::new_user_test_id(102); + + let sub1_day1 = NodeMetricsHistoryResponse { + timestamp_nanos: 1, + node_metrics: vec![ + NodeMetrics { + node_id: node1, + num_blocks_proposed_total: 10, + num_block_failures_total: 2, + }, + NodeMetrics { + node_id: node2, + num_blocks_proposed_total: 20, + num_block_failures_total: 5, + }, + ], + }; + + let sub1_day2 = NodeMetricsHistoryResponse { + timestamp_nanos: 2, + node_metrics: vec![ + NodeMetrics { + node_id: node1, + num_blocks_proposed_total: 20, + num_block_failures_total: 12, + }, + NodeMetrics { + node_id: node2, + num_blocks_proposed_total: 25, + num_block_failures_total: 8, + }, + ], + }; + + // This happens when the node gets redeployed + let sub1_day3 = NodeMetricsHistoryResponse { + timestamp_nanos: 3, + node_metrics: vec![NodeMetrics { + node_id: node1, + num_blocks_proposed_total: 15, + num_block_failures_total: 3, + }], + }; + + // Simulating subnet change + let sub2_day3 = NodeMetricsHistoryResponse { + timestamp_nanos: 3, + node_metrics: vec![NodeMetrics { + node_id: node2, + num_blocks_proposed_total: 35, + num_block_failures_total: 10, + }], + }; + + let input_metrics = HashMap::from([ + (subnet1, vec![sub1_day1, sub1_day2, sub1_day3]), + (subnet2, vec![sub2_day3]), + ]); + + let result = daily_node_metrics(input_metrics); + + let metrics_node1 = result.get(&node1).expect("Node1 metrics not found"); + assert_eq!(metrics_node1[0].subnet_assigned, subnet1); + assert_eq!(metrics_node1[0].num_blocks_proposed, 10); + assert_eq!(metrics_node1[0].num_blocks_failed, 2); + + assert_eq!(metrics_node1[1].subnet_assigned, subnet1); + assert_eq!(metrics_node1[1].num_blocks_proposed, 10); + assert_eq!(metrics_node1[1].num_blocks_failed, 10); + + assert_eq!(metrics_node1[2].subnet_assigned, subnet1); + assert_eq!(metrics_node1[2].num_blocks_proposed, 15); + assert_eq!(metrics_node1[2].num_blocks_failed, 3); + + let metrics_node2 = result.get(&node2).expect("Node2 metrics not found"); + assert_eq!(metrics_node2[0].subnet_assigned, subnet1); + assert_eq!(metrics_node2[0].num_blocks_proposed, 20); + assert_eq!(metrics_node2[0].num_blocks_failed, 5); + + assert_eq!(metrics_node2[1].subnet_assigned, subnet1); + assert_eq!(metrics_node2[1].num_blocks_proposed, 5); + assert_eq!(metrics_node2[1].num_blocks_failed, 3); + + assert_eq!(metrics_node2[2].subnet_assigned, subnet2); + assert_eq!(metrics_node2[2].num_blocks_proposed, 10); + assert_eq!(metrics_node2[2].num_blocks_failed, 2); +} +#[test] +fn test_rewards_percent() { + let mut logger = RewardsLog::default(); + let daily_fr: Vec = daily_mocked_failure_rates(vec![ + // Avg. failure rate = 0.4 + MockedMetrics::new(20, 6, 4), + // Avg. failure rate = 0.2 + MockedMetrics::new(20, 8, 2), + ]); + + let result = assigned_multiplier(&mut logger, daily_fr); + // Avg. failure rate = 0.3 -> 1 - (0.3-0.1) / (0.6-0.1) * 0.8 = 0.68 + assert_eq!(result, dec!(0.68)); + + let daily_fr: Vec = daily_mocked_failure_rates(vec![ + // Avg. failure rate = 0.5 + MockedMetrics::new(1, 5, 5), + ]); + let result = assigned_multiplier(&mut logger, daily_fr); + // Avg. failure rate = 0.5 -> 1 - (0.5-0.1) / (0.6-0.1) * 0.8 = 0.36 + assert_eq!(result, dec!(0.36)); + + let daily_fr: Vec = daily_mocked_failure_rates(vec![ + // Avg. failure rate = 0.6666666667 + MockedMetrics::new(1, 200, 400), + // Avg. failure rate = 0.8333333333 + MockedMetrics::new(1, 5, 25), // no penalty + ]); + let result = assigned_multiplier(&mut logger, daily_fr); + // Avg. failure rate = (0.6666666667 + 0.8333333333) / 2 = 0.75 + // 1 - (0.75-0.1) / (0.6-0.1) * 0.8 = 0.2 + assert_eq!(result, dec!(0.2)); +} + +#[test] +fn test_rewards_percent_max_reduction() { + let mut logger = RewardsLog::default(); + + let daily_fr: Vec = daily_mocked_failure_rates(vec![ + // Avg. failure rate = 0.95 + MockedMetrics::new(10, 5, 95), + ]); + let result = assigned_multiplier(&mut logger, daily_fr); + assert_eq!(result, dec!(0.2)); +} + +#[test] +fn test_rewards_percent_min_reduction() { + let mut logger = RewardsLog::default(); + + let daily_fr: Vec = daily_mocked_failure_rates(vec![ + // Avg. failure rate = 0.1 + MockedMetrics::new(10, 9, 1), + ]); + let result = assigned_multiplier(&mut logger, daily_fr); + assert_eq!(result, dec!(1)); +} + +#[test] +fn test_same_rewards_percent_if_gaps_no_penalty() { + let mut logger = RewardsLog::default(); + let gap = MockedMetrics::new(1, 10, 0); + let daily_fr_mid_gap: Vec = daily_mocked_failure_rates(vec![ + MockedMetrics::new(1, 6, 4), + gap.clone(), + MockedMetrics::new(1, 7, 3), + ]); + let daily_fr_left_gap: Vec = daily_mocked_failure_rates(vec![ + gap.clone(), + MockedMetrics::new(1, 6, 4), + MockedMetrics::new(1, 7, 3), + ]); + let daily_fr_right_gap: Vec = daily_mocked_failure_rates(vec![ + gap.clone(), + MockedMetrics::new(1, 6, 4), + MockedMetrics::new(1, 7, 3), + ]); + + assert_eq!( + assigned_multiplier(&mut logger, daily_fr_mid_gap.clone()), + dec!(0.7866666666666666666666666667) + ); + + assert_eq!( + assigned_multiplier(&mut logger, daily_fr_mid_gap.clone()), + assigned_multiplier(&mut logger, daily_fr_left_gap.clone()) + ); + assert_eq!( + assigned_multiplier(&mut logger, daily_fr_right_gap.clone()), + assigned_multiplier(&mut logger, daily_fr_left_gap) + ); +} + +fn from_subnet_daily_metrics( + subnet_id: PrincipalId, + daily_subnet_fr: Vec<(TimestampNanos, Vec)>, +) -> HashMap> { + let mut daily_node_metrics = HashMap::new(); + for (day, fr) in daily_subnet_fr { + fr.into_iter().enumerate().for_each(|(i, fr)| { + let node_metrics: &mut Vec = daily_node_metrics + .entry(PrincipalId::new_user_test_id(i as u64)) + .or_default(); + + node_metrics.push(DailyNodeMetrics { + ts: day, + subnet_assigned: subnet_id, + failure_rate: Decimal::from_f64(fr).unwrap(), + ..DailyNodeMetrics::default() + }); + }); + } + daily_node_metrics +} +#[test] +fn test_systematic_fr_calculation() { + let subnet1 = PrincipalId::new_user_test_id(10); + + let assigned_metrics = from_subnet_daily_metrics( + subnet1, + vec![ + (1, vec![0.2, 0.21, 0.1, 0.9, 0.3]), // Ordered: [0.1, 0.2, 0.21, * 0.3, 0.9] + (2, vec![0.8, 0.9, 0.5, 0.6, 0.7]), // Ordered: [0.5, 0.6, 0.7, * 0.8, 0.9] + (3, vec![0.5, 0.6, 0.64, 0.8]), // Ordered: [0.5, 0.6, * 0.64, 0.8] + (4, vec![0.5, 0.6]), // Ordered: [0.5, * 0.6] + (5, vec![0.2, 0.21, 0.1, 0.9, 0.3, 0.23]), // Ordered: [0.1, 0.2, 0.21, 0.23, * 0.3, 0.9] + ], + ); + + let result = systematic_fr_per_subnet(&assigned_metrics); + + let expected: HashMap<(PrincipalId, TimestampNanos), Decimal> = HashMap::from([ + ((subnet1, 1), dec!(0.3)), + ((subnet1, 2), dec!(0.8)), + ((subnet1, 3), dec!(0.64)), + ((subnet1, 4), dec!(0.6)), + ((subnet1, 5), dec!(0.3)), + ]); + + assert_eq!(result, expected); +} + +#[test] +fn test_idiosyncratic_daily_fr_correct_values() { + let node1 = PrincipalId::new_user_test_id(1); + let node2 = PrincipalId::new_user_test_id(2); + let subnet1 = PrincipalId::new_user_test_id(10); + + let assigned_metrics = HashMap::from([ + ( + node1, + vec![ + DailyNodeMetrics::from_fr_dummy(1, subnet1, dec!(0.2)), + DailyNodeMetrics::from_fr_dummy(2, subnet1, dec!(0.5)), + DailyNodeMetrics::from_fr_dummy(3, subnet1, dec!(0.849)), + ], + ), + ( + node2, + vec![DailyNodeMetrics::from_fr_dummy(1, subnet1, dec!(0.5))], + ), + ]); + + let subnets_systematic_fr = HashMap::from([ + ((subnet1, 1), dec!(0.1)), + ((subnet1, 2), dec!(0.2)), + ((subnet1, 3), dec!(0.1)), + ]); + + let result = idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); + + let expected = HashMap::from([ + (node1, vec![dec!(0.1), dec!(0.3), dec!(0.749)]), // (0.2 - 0.1), (0.5 - 0.2), (0.849 - 0.1) + (node2, vec![dec!(0.4)]), // (0.5 - 0.1) + ]); + + assert_eq!(result, expected); +} + +#[test] +#[should_panic(expected = "Systematic failure rate not found")] +fn test_idiosyncratic_daily_fr_missing_systematic_fr() { + let node1 = PrincipalId::new_user_test_id(1); + let subnet1 = PrincipalId::new_user_test_id(10); + + let assigned_metrics = HashMap::from([( + node1, + vec![DailyNodeMetrics::from_fr_dummy(1, subnet1, dec!(0.2))], + )]); + + let subnets_systematic_fr = HashMap::from([((subnet1, 2), dec!(0.1))]); + + idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); +} + +#[test] +fn test_idiosyncratic_daily_fr_negative_failure_rate() { + let node1 = PrincipalId::new_user_test_id(1); + let subnet1 = PrincipalId::new_user_test_id(10); + + let assigned_metrics = HashMap::from([( + node1, + vec![DailyNodeMetrics::from_fr_dummy(1, subnet1, dec!(0.05))], + )]); + + let subnets_systematic_fr = HashMap::from([((subnet1, 1), dec!(0.1))]); + + let result = idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); + + // Expecting zero due to saturation + let expected = HashMap::from([(node1, vec![Decimal::ZERO])]); + + assert_eq!(result, expected); +} + +#[test] +fn test_node_provider_rewards_no_nodes() { + let mut logger = RewardsLog::default(); + let rewardables = vec![]; + let nodes_idiosyncratic_fr = HashMap::new(); + let days_in_period = 30; + let rewards_table = NodeRewardsTable::default(); + + let rewards = node_provider_rewards( + &mut logger, + &rewardables, + nodes_idiosyncratic_fr, + days_in_period, + &rewards_table, + ); + + assert_eq!(rewards.xdr_permyriad, 0); + assert_eq!(rewards.xdr_permyriad_no_reduction, 0); +} + +#[test] +fn test_node_provider_below_min_limit() { + let mut logger = RewardsLog::default(); + let node_provider_id = PrincipalId::new_anonymous(); + let rewardables = vec![ + RewardableNode { + node_id: PrincipalId::new_user_test_id(1), + node_provider_id, + region: "region1".to_string(), + node_type: "type1".to_string(), + }, + RewardableNode { + node_id: PrincipalId::new_user_test_id(2), + node_provider_id, + region: "region1".to_string(), + node_type: "type3.1".to_string(), + }, + ]; + let nodes_idiosyncratic_fr = HashMap::new(); + let days_in_period = 30; + let rewards_table = NodeRewardsTable::default(); + + let rewards = node_provider_rewards( + &mut logger, + &rewardables, + nodes_idiosyncratic_fr, + days_in_period, + &rewards_table, + ); + + assert_eq!(rewards.xdr_permyriad, 2); + assert_eq!(rewards.xdr_permyriad_no_reduction, 2); +} + +fn helper_dummy_rewardables(node_id: PrincipalId, node_provider_id: PrincipalId) -> RewardableNode { + RewardableNode { + node_id, + node_provider_id, + region: "A,B".to_string(), + node_type: "type1".to_string(), + } +} + +#[test] +fn test_node_provider_rewards_one_assigned() { + let mut logger = RewardsLog::default(); + let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); + let days_in_period = 30; + + let rewardables = (1..=5) + .map(|i| { + helper_dummy_rewardables( + PrincipalId::new_user_test_id(i), + PrincipalId::new_anonymous(), + ) + }) + .collect_vec(); + + let mut nodes_idiosyncratic_fr = HashMap::new(); + nodes_idiosyncratic_fr.insert( + PrincipalId::new_user_test_id(1), + vec![dec!(0.4), dec!(0.2), dec!(0.3), dec!(0.4)], // Avg. 0.325 + ); + + let rewards = node_provider_rewards( + &mut logger, + &rewardables, + nodes_idiosyncratic_fr, + days_in_period, + &node_rewards_table, + ); + + // Unassigned failure rate: 0.325 + // Unassigned multiplier: 1 - (0.325-0.1) / (0.6-0.1) * 0.8 = 0.64 Rewards: 1000 * 0.64 = 640 XDRs + // Total rewards: 640 * 5 = 3200 XDRs + assert_eq!(rewards.xdr_permyriad, 3200); + assert_eq!(rewards.xdr_permyriad_no_reduction, 5000); +} + +#[test] +fn test_node_provider_rewards_two_assigned() { + let mut logger = RewardsLog::default(); + let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); + let days_in_period = 30; + + let rewardables = (1..=5) + .map(|i| { + helper_dummy_rewardables( + PrincipalId::new_user_test_id(i), + PrincipalId::new_anonymous(), + ) + }) + .collect_vec(); + + let mut nodes_idiosyncratic_fr = HashMap::new(); + nodes_idiosyncratic_fr.insert( + PrincipalId::new_user_test_id(1), + vec![dec!(0.4), dec!(0.2), dec!(0.3), dec!(0.4)], // Avg. 0.325 + ); + nodes_idiosyncratic_fr.insert( + PrincipalId::new_user_test_id(2), + vec![dec!(0.9), dec!(0.6), dec!(0.304), dec!(0.102)], // Avg. 0.4765 + ); + + let rewards = node_provider_rewards( + &mut logger, + &rewardables, + nodes_idiosyncratic_fr, + days_in_period, + &node_rewards_table, + ); + + // Avg. assigned failure rate: (0.325 + 0.4765) / 2 = 0.40075 + // 3 nodes are unassigned in the period: + // Unassigned failure rate: 0.40075 + // Unassigned multiplier: 1 - (0.40075-0.1) / (0.6-0.1) * 0.8 = 0.51880 + // Rewards: 1000 * 0.51880 = 518.80 XDRs + // 2 nodes are assigned in the period: + // node1: + // failure rate = (0.325 * 4 + 0.40075 * 26) / 30 = 0.390 + // multiplier = 1 - (0.390-0.1) / (0.6-0.1) * 0.8 = 0.53496 + // Rewards: 1000 * 0.53496 = 534.96 XDRs + // node2: + // failure rate = (0.4765 * 4 + 0.40075 * 26) / 30 = 0.41 + // multiplier = 1 - (0.41-0.1) / (0.6-0.1) * 0.8 = 0.50264 + // Rewards: 1000 * 0.50264 = 502.64 XDRs + // Total rewards: 518.80 * 3 + 534.96 + 502.64 = 2594 XDRs + assert_eq!(rewards.xdr_permyriad, 2594); + assert_eq!(rewards.xdr_permyriad_no_reduction, 5000); +} + +// #[test] +// fn test_np_rewards_type3_coeff() { +// let mut logger = RewardsLog::default(); +// let mut assigned_multipliers: HashMap> = +// HashMap::default(); +// let mut rewardable_nodes: HashMap = HashMap::default(); +// let region_node_type = ("A,B,C".to_string(), "type3.1".to_string()); +// +// // 4 nodes in period: 1 assigned, 3 unassigned +// rewardable_nodes.insert(region_node_type.clone(), 4); +// assigned_multipliers.insert(region_node_type, vec![dec!(0.5)]); +// let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); +// let rewards = node_provider_rewards( +// &mut logger, +// &assigned_multipliers, +// &rewardable_nodes, +// &node_rewards_table, +// ); +// +// let rewardables = vec![RewardableNode { +// node_id: PrincipalId::new_user_test_id(1), +// node_provider_id: PrincipalId::new_user_test_id(2), +// region: "A,B,C".to_string(), +// node_type: "type3.1".to_string(), +// }]; +// let mut assigned_metrics = HashMap::new(); +// assigned_metrics.insert( +// PrincipalId::new_user_test_id(1), +// vec![DailyNodeMetrics::new( +// 0, +// PrincipalId::new_user_test_id(1), +// 10, +// 1, +// )], +// ); +// let subnets_systematic_fr = HashMap::new(); +// let days_in_period = 30; +// let rewards_table = NodeRewardsTable::default(); +// +// let rewards = node_provider_rewards( +// &mut logger, +// &rewardables, +// &assigned_metrics, +// &subnets_systematic_fr, +// days_in_period, +// &rewards_table, +// ); +// +// // Coefficients avg., operation=avg(0.95,0.95,0.95,0.95), result=0.95 +// // Rewards avg., operation=avg(1500,1500,1500,1500), result=1500 +// // Total rewards after coefficient reduction, operation=sum(1500 * 1,1500 * 0.95,1500 * 0.9025,1500 * 0.8574), result=5564 +// +// // Rewards average after coefficient reduction, operation=5564 / 4, result=1391 +// // Total XDR no penalties, operation=sum(1391,1391,1391,1391), result=5564 +// assert_eq!(rewards.xdr_permyriad_no_reduction, 5564); +// +// // Total XDR, operation=sum(1391 * 0.5,1391 * 0.5,1391 * 0.5,1391 * 0.5), result=2782 +// assert_eq!(rewards.xdr_permyriad, 2782); +// } + +// #[test] +// fn test_np_rewards_type3_coeff() { +// let mut logger = RewardsLog::default(); +// let mut assigned_multipliers: HashMap> = +// HashMap::default(); +// let mut rewardable_nodes: HashMap = HashMap::default(); +// let region_node_type = ("A,B,C".to_string(), "type3.1".to_string()); +// +// // 4 nodes in period: 1 assigned, 3 unassigned +// rewardable_nodes.insert(region_node_type.clone(), 4); +// assigned_multipliers.insert(region_node_type, vec![dec!(0.5)]); +// let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); +// let rewards = node_provider_rewards( +// &mut logger, +// &assigned_multipliers, +// &rewardable_nodes, +// &node_rewards_table, +// ); +// +// // Coefficients avg., operation=avg(0.95,0.95,0.95,0.95), result=0.95 +// // Rewards avg., operation=avg(1500,1500,1500,1500), result=1500 +// // Total rewards after coefficient reduction, operation=sum(1500 * 1,1500 * 0.95,1500 * 0.9025,1500 * 0.8574), result=5564 +// +// // Rewards average after coefficient reduction, operation=5564 / 4, result=1391 +// // Total XDR no penalties, operation=sum(1391,1391,1391,1391), result=5564 +// assert_eq!(rewards.xdr_permyriad_no_reduction, 5564); +// +// // Total XDR, operation=sum(1391 * 0.5,1391 * 0.5,1391 * 0.5,1391 * 0.5), result=2782 +// assert_eq!(rewards.xdr_permyriad, 2782); +// } + +// #[test] +// fn test_np_rewards_type3_mix() { +// let mut logger = RewardsLog::default(); +// let mut assigned_multipliers: HashMap> = +// HashMap::default(); +// let mut rewardable_nodes: HashMap = HashMap::default(); +// +// // 5 nodes in period: 2 assigned, 3 unassigned +// assigned_multipliers.insert( +// ("A,B,D".to_string(), "type3".to_string()), +// vec![dec!(0.5), dec!(0.4)], +// ); +// +// // This will take rates from outer +// rewardable_nodes.insert(("A,B,D".to_string(), "type3".to_string()), 3); +// +// // This will take rates from inner +// rewardable_nodes.insert(("A,B,C".to_string(), "type3.1".to_string()), 2); +// +// let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); +// let rewards = node_provider_rewards( +// &mut logger, +// &assigned_multipliers, +// &rewardable_nodes, +// &node_rewards_table, +// ); +// +// // Coefficients avg(0.95,0.95,0.97,0.97,0.97) = 0.9620 +// // Rewards avg., operation=avg(1500,1500,1000,1000,1000), result=1200 +// // Rewards average sum(1200 * 1,1200 * 0.9620,1200 * 0.9254,1200 * 0.8903,1200 * 0.8564) / 5, result=1112 +// // Unassigned Nodes Multiplier, operation=avg(0.5,0.4), result=0.450 +// +// // Total XDR, operation=sum(1112 * 0.450,1112 * 0.450,1112 * 0.5,1112 * 0.4,1112 * 0.450), result=2502 +// assert_eq!(rewards.xdr_permyriad, 2502); +// // Total XDR no penalties, operation=1112 * 5, result=5561 +// assert_eq!(rewards.xdr_permyriad_no_reduction, 5561); +// } diff --git a/rs/registry/node_provider_rewards/src/v1_types.rs b/rs/registry/node_provider_rewards/src/v1_types.rs index 83fd82f89e3..e96fe216f2a 100644 --- a/rs/registry/node_provider_rewards/src/v1_types.rs +++ b/rs/registry/node_provider_rewards/src/v1_types.rs @@ -2,87 +2,56 @@ use std::collections::HashMap; use candid::CandidType; use ic_base_types::PrincipalId; -use ic_management_canister_types::{NodeMetrics, NodeMetricsHistoryResponse}; +use ic_management_canister_types::NodeMetricsHistoryResponse; +use num_traits::FromPrimitive; +use rust_decimal::Decimal; use serde::Deserialize; use crate::v1_logs::RewardsLog; pub type NodeMultiplierStats = (PrincipalId, MultiplierStats); -pub type RewardablesWithNodesMetrics = ( - HashMap, - HashMap>, -); pub type RegionNodeTypeCategory = (String, String); pub type TimestampNanos = u64; +pub type SubnetMetricsHistory = (PrincipalId, Vec); + #[derive(Clone, Hash, Eq, PartialEq)] pub struct RewardableNode { pub node_id: PrincipalId, pub node_provider_id: PrincipalId, pub region: String, pub node_type: String, - pub node_metrics: Option>, } -#[derive(Clone, Hash, Eq, PartialEq)] +#[derive(Clone, Hash, Eq, PartialEq, Debug, Default)] pub struct DailyNodeMetrics { + pub ts: u64, + pub subnet_assigned: PrincipalId, pub num_blocks_proposed: u64, pub num_blocks_failed: u64, + pub failure_rate: Decimal, } -pub struct NodesMetricsHistory(Vec); - -impl From for HashMap> { - fn from(nodes_metrics: NodesMetricsHistory) -> Self { - let mut sorted_metrics = nodes_metrics.0; - sorted_metrics.sort_by_key(|metrics| metrics.timestamp_nanos); - let mut sorted_metrics_per_node: HashMap> = - HashMap::default(); - - for metrics in sorted_metrics { - for node_metrics in metrics.node_metrics { - sorted_metrics_per_node - .entry(node_metrics.node_id) - .or_default() - .push(node_metrics); - } +impl DailyNodeMetrics { + pub fn new( + ts: u64, + subnet_assigned: PrincipalId, + num_blocks_proposed: u64, + num_blocks_failed: u64, + ) -> Self { + let daily_total = num_blocks_proposed + num_blocks_failed; + let failure_rate = if daily_total == 0 { + Decimal::ZERO + } else { + Decimal::from_f64(num_blocks_failed as f64 / daily_total as f64).unwrap() + }; + DailyNodeMetrics { + ts, + num_blocks_proposed, + num_blocks_failed, + subnet_assigned, + failure_rate, } - - sorted_metrics_per_node - .into_iter() - .map(|(node_id, metrics)| { - let mut daily_node_metrics = Vec::new(); - let mut previous_proposed_total = 0; - let mut previous_failed_total = 0; - - for node_metrics in metrics { - let current_proposed_total = node_metrics.num_blocks_proposed_total; - let current_failed_total = node_metrics.num_block_failures_total; - - let (num_blocks_proposed, num_blocks_failed) = if previous_failed_total - > current_failed_total - || previous_proposed_total > current_proposed_total - { - // This is the case when node is deployed again - (current_proposed_total, current_failed_total) - } else { - ( - current_proposed_total - previous_proposed_total, - current_failed_total - previous_failed_total, - ) - }; - - daily_node_metrics.push(DailyNodeMetrics { - num_blocks_proposed, - num_blocks_failed, - }); - - previous_proposed_total = num_blocks_proposed; - previous_failed_total = num_blocks_failed; - } - (node_id, daily_node_metrics) - }) - .collect() } } @@ -98,7 +67,7 @@ pub struct MultiplierStats { } pub struct RewardsPerNodeProvider { - pub rewards_per_node_provider: HashMap)>, + pub rewards_per_node_provider: HashMap, pub rewards_log_per_node_provider: HashMap, } From 843a522b637139e711ead1bce87b85b63ee1513d Mon Sep 17 00:00:00 2001 From: Pietro Date: Tue, 10 Dec 2024 14:01:59 +0100 Subject: [PATCH 11/20] Adjust logs and add comments --- .../node_provider_rewards/src/v1_logs.rs | 218 ++++----- .../node_provider_rewards/src/v1_rewards.rs | 171 ++++--- .../src/v1_rewards/tests.rs | 420 +++++++++++------- .../node_provider_rewards/src/v1_types.rs | 14 +- 4 files changed, 489 insertions(+), 334 deletions(-) diff --git a/rs/registry/node_provider_rewards/src/v1_logs.rs b/rs/registry/node_provider_rewards/src/v1_logs.rs index 208daa6c456..ecc8b7e80a6 100644 --- a/rs/registry/node_provider_rewards/src/v1_logs.rs +++ b/rs/registry/node_provider_rewards/src/v1_logs.rs @@ -1,8 +1,13 @@ +use crate::v1_types::DailyNodeMetrics; use ic_base_types::PrincipalId; use itertools::Itertools; use rust_decimal::{prelude::Zero, Decimal}; use std::fmt; +fn round_dp_4(dec: &Decimal) -> Decimal { + dec.round_dp(4) +} + #[derive(Clone)] pub enum Operation { Sum(Vec), @@ -55,39 +60,45 @@ impl fmt::Display for Operation { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let (symbol, o1, o2) = match self { Operation::Sum(values) => { - return write!(f, "{}", Operation::format_values(values, "sum")) + return write!( + f, + "{}", + Operation::format_values( + &values.iter().map(|v| round_dp_4(v)).collect_vec(), + "sum" + ) + ) } Operation::SumOps(operations) => { return write!(f, "{}", Operation::format_values(operations, "sum")) } Operation::Avg(values) => { - return write!(f, "{}", Operation::format_values(values, "avg")) + return write!( + f, + "{}", + Operation::format_values( + &values.iter().map(|v| round_dp_4(v)).collect_vec(), + "avg" + ) + ) } Operation::Subtract(o1, o2) => ("-", o1, o2), Operation::Divide(o1, o2) => ("/", o1, o2), Operation::Multiply(o1, o2) => ("*", o1, o2), Operation::Set(o1) => return write!(f, "set {}", o1), }; - write!(f, "{} {} {}", o1.round_dp(4), symbol, o2.round_dp(4)) + + write!(f, "{} {} {}", round_dp_4(o1), symbol, round_dp_4(o2)) } } pub enum LogEntry { - RewardsForNodeProvider(PrincipalId, u32), - RewardMultiplierForNode(PrincipalId, Decimal), - RewardsXDRTotal(Decimal), + RewardsXDRTotal(Decimal, Decimal), Execute { reason: String, operation: Operation, result: Decimal, }, - PerformanceBasedRewardables { - node_type: String, - region: String, - count: usize, - assigned_multipliers: Vec, - unassigned_multipliers: Vec, - }, RateNotFoundInRewardTable { node_type: String, region: String, @@ -97,19 +108,32 @@ pub enum LogEntry { region: String, coeff: Decimal, base_rewards: Decimal, + node_count: u32, }, - AvgType3Rewards { - region: String, - rewards_avg: Decimal, - coefficients_avg: Decimal, - region_rewards_avg: Decimal, + ActiveIdiosyncraticFailureRates { + node_id: PrincipalId, + daily_metrics: Vec, + failure_rates: Vec, }, - UnassignedMultiplier(Decimal), - NodeCountRewardables { + ComputeRewardsForNode { + node_id: PrincipalId, node_type: String, region: String, - count: usize, }, + CalculateRewardsForNodeProvider(PrincipalId), + BaseRewards(Decimal), + IdiosyncraticFailureRates(Vec), + RewardsReductionPercent { + failure_rate: Decimal, + min_fr: Decimal, + max_fr: Decimal, + max_rr: Decimal, + rewards_reduction: Decimal, + }, + ComputeBaseRewardsForRegionNodeType, + ComputeUnassignedFailureRate, + NodeStatusAssigned, + NodeStatusUnassigned, } impl fmt::Display for LogEntry { @@ -120,40 +144,20 @@ impl fmt::Display for LogEntry { operation, result, } => { - write!( - f, - "ExecuteOperation | reason={}, operation={}, result={}", - reason, - operation, - result.round_dp(2) - ) - } - LogEntry::RewardsForNodeProvider(principal, node_count) => { - write!( - f, - "Node Provider: {} rewardable nodes in period: {}", - principal, node_count - ) - } - LogEntry::RewardMultiplierForNode(principal, multiplier) => { - write!( - f, - "Rewards Multiplier for node: {} is {}", - principal, - multiplier.round_dp(2) - ) + write!(f, "{}: {} = {}", reason, operation, round_dp_4(result)) } - LogEntry::RewardsXDRTotal(rewards_xdr_total) => { + LogEntry::RewardsXDRTotal(rewards_xdr_total, rewards_xdr_total_adjusted) => { write!( f, - "Total rewards XDR permyriad: {}", - rewards_xdr_total.round_dp(2) + "Total rewards XDR permyriad: {}\nTotal rewards XDR permyriad not adjusted: {}", + round_dp_4(rewards_xdr_total), + round_dp_4(rewards_xdr_total_adjusted) ) } LogEntry::RateNotFoundInRewardTable { node_type, region } => { write!( f, - "RateNotFoundInRewardTable | node_type={}, region={}", + "RateNotFoundInRewardTable | node_type: {}, region: {}", node_type, region ) } @@ -162,80 +166,91 @@ impl fmt::Display for LogEntry { region, coeff, base_rewards, + node_count, } => { write!( f, - "RewardTableEntry | node_type={}, region={}, coeff={}, base_rewards={}", - node_type, region, coeff, base_rewards + "node_type: {}, region: {}, coeff: {}, base_rewards: {}, node_count: {}", + node_type, region, coeff, base_rewards, node_count ) } - LogEntry::PerformanceBasedRewardables { - node_type, - region, - count, - assigned_multipliers: assigned_multiplier, - unassigned_multipliers: unassigned_multiplier, + LogEntry::ActiveIdiosyncraticFailureRates { + node_id, + daily_metrics, + failure_rates, } => { write!( f, - "Region {} with type: {} | Rewardable Nodes: {} Assigned Multipliers: {:?} Unassigned Multipliers: {:?}", - region, - node_type, - count, - assigned_multiplier.iter().map(|dec| dec.round_dp(2)).collect_vec(), - unassigned_multiplier.iter().map(|dec| dec.round_dp(2)).collect_vec() + "ActiveIdiosyncraticFailureRates | node_id={}, daily_metrics={:?}, failure_rates={}", + node_id, daily_metrics, failure_rates.len() ) } - LogEntry::AvgType3Rewards { + LogEntry::ComputeRewardsForNode { + node_id, + node_type, region, - rewards_avg, - coefficients_avg, - region_rewards_avg, } => { write!( f, - "Avg. rewards for nodes with type: type3* in region: {} is {}\nRegion rewards average: {}\nReduction coefficient average:{}", - region, - rewards_avg.round_dp(2), - region_rewards_avg, - coefficients_avg + "Compute Rewards For Node | node_id={}, node_type={}, region={}", + node_id, node_type, region ) } - LogEntry::UnassignedMultiplier(unassigned_multiplier) => { + LogEntry::CalculateRewardsForNodeProvider(node_provider_id) => { write!( f, - "Unassigned Nodes Multiplier: {}", - unassigned_multiplier.round_dp(2) + "CalculateRewardsForNodeProvider | node_provider_id={}", + node_provider_id ) } - LogEntry::NodeCountRewardables { - node_type, - region, - count, + LogEntry::BaseRewards(rewards_xdr) => { + write!(f, "Base rewards XDRs: {}", round_dp_4(rewards_xdr)) + } + LogEntry::IdiosyncraticFailureRates(failure_rates) => { + write!( + f, + "Idiosyncratic daily failure rates : {}", + failure_rates.iter().map(|dec| dec).join(",") + ) + } + LogEntry::RewardsReductionPercent { + failure_rate, + min_fr, + max_fr, + max_rr, + rewards_reduction, } => { write!( f, - "Region {} with type: {} | Rewardable Nodes: {} Rewarded independently of their performance", - region, node_type, count + "Rewards reduction percent: ({} - {}) / ({} - {}) * {} = {}", + round_dp_4(failure_rate), + min_fr, + max_fr, + min_fr, + max_rr, + round_dp_4(rewards_reduction) ) } + LogEntry::ComputeBaseRewardsForRegionNodeType => { + write!(f, "Compute Base Rewards For RegionNodeType") + } + LogEntry::ComputeUnassignedFailureRate => { + write!(f, "Compute Unassigned Days Failure Rate") + } + LogEntry::NodeStatusAssigned => { + write!(f, "Node status: Assigned") + } + LogEntry::NodeStatusUnassigned => { + write!(f, "Node status: Unassigned") + } } } } -#[derive(Copy, Clone)] pub enum LogLevel { - Info, - Debug, -} - -impl fmt::Display for LogLevel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - LogLevel::Info => write!(f, "INFO"), - LogLevel::Debug => write!(f, "DEBUG"), - } - } + High, + Mid, + Low, } #[derive(Default)] @@ -244,8 +259,8 @@ pub struct RewardsLog { } impl RewardsLog { - pub fn add_entry(&mut self, entry: LogEntry) { - self.entries.push((LogLevel::Info, entry)); + pub fn add_entry(&mut self, log_level: LogLevel, entry: LogEntry) { + self.entries.push((log_level, entry)); } pub fn execute(&mut self, reason: &str, operation: Operation) -> Decimal { @@ -255,19 +270,18 @@ impl RewardsLog { operation, result, }; - self.entries.push((LogLevel::Debug, entry)); + self.add_entry(LogLevel::Mid, entry); result } - pub fn get_log(&self, level: LogLevel) -> Vec { + pub fn get_log(&self) -> String { self.entries .iter() - .filter_map( - move |(entry_log_level, entry)| match (level, entry_log_level) { - (LogLevel::Info, LogLevel::Debug) => None, - _ => Some(format!("{}: {} ", level, entry)), - }, - ) - .collect_vec() + .map(|(log_level, entry)| match log_level { + LogLevel::High => format!("\x1b[1m{}\x1b[0m", entry), + LogLevel::Mid => format!(" - {}", entry), + LogLevel::Low => format!(" - {}", entry), + }) + .join("\n") } } diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index dafb77382fa..87b26afc705 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -3,6 +3,7 @@ use ic_protobuf::registry::node_rewards::v2::{NodeRewardRate, NodeRewardsTable}; use itertools::Itertools; use num_traits::ToPrimitive; +use crate::v1_logs::LogLevel; use crate::v1_types::TimestampNanos; use crate::{ v1_logs::{LogEntry, Operation, RewardsLog}, @@ -18,6 +19,7 @@ use std::collections::HashMap; const FULL_REWARDS_MACHINES_LIMIT: u32 = 4; const MIN_FAILURE_RATE: Decimal = dec!(0.1); const MAX_FAILURE_RATE: Decimal = dec!(0.6); +const MAX_REWARDS_REDUCTION: Decimal = dec!(0.8); const RF: &str = "Linear Reduction factor"; // The algo works as follows: @@ -43,6 +45,11 @@ pub fn calculate_rewards( for (node_provider_id, node_provider_rewardables) in node_provider_rewardables { let mut logger = RewardsLog::default(); + logger.add_entry( + LogLevel::High, + LogEntry::CalculateRewardsForNodeProvider(node_provider_id), + ); + let assigned_metrics: HashMap> = node_provider_rewardables .iter() @@ -53,7 +60,7 @@ pub fn calculate_rewards( }) .collect::>>(); let idiosyncratic_daily_fr = - idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); + idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); let rewards = node_provider_rewards( &mut logger, @@ -74,17 +81,19 @@ pub fn calculate_rewards( } fn idiosyncratic_daily_fr( + logger: &mut RewardsLog, assigned_metrics: &HashMap>, subnets_systematic_fr: &HashMap<(PrincipalId, TimestampNanos), Decimal>, ) -> HashMap> { let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); for (node_id, daily_metrics) in assigned_metrics { + let failure_rates = nodes_idiosyncratic_fr.entry(*node_id).or_default(); + for metrics in daily_metrics { let systematic_fr = subnets_systematic_fr .get(&(metrics.subnet_assigned, metrics.ts)) .expect("Systematic failure rate not found"); - let failure_rates = nodes_idiosyncratic_fr.entry(*node_id).or_default(); let fr = if metrics.failure_rate < *systematic_fr { Decimal::ZERO } else { @@ -92,6 +101,14 @@ fn idiosyncratic_daily_fr( }; failure_rates.push(fr); } + logger.add_entry( + LogLevel::High, + LogEntry::ActiveIdiosyncraticFailureRates { + node_id: *node_id, + daily_metrics: daily_metrics.clone(), + failure_rates: failure_rates.clone(), + }, + ); } nodes_idiosyncratic_fr @@ -113,45 +130,62 @@ fn node_provider_rewards( let rewardable_nodes_count = rewardables.len() as u32; let mut nodes_idiosyncratic_fr = nodes_idiosyncratic_fr; + // 0. Compute base rewards for each region and node type + logger.add_entry( + LogLevel::High, + LogEntry::ComputeBaseRewardsForRegionNodeType, + ); for node in rewardables { // Count the number of nodes per region and node type let nodes_count = region_node_type_rewardables .entry((node.region.clone(), node.node_type.clone())) .or_default(); *nodes_count += 1; + } + let region_nodetype_rewards: HashMap = + base_rewards_region_nodetype(logger, ®ion_node_type_rewardables, rewards_table); + // 1. Compute the unassigned failure rate + logger.add_entry(LogLevel::High, LogEntry::ComputeUnassignedFailureRate); + for node in rewardables { // 1. Compute the unassigned failure rate if let Some(daily_fr) = nodes_idiosyncratic_fr.get(&node.node_id) { - let assigned_fr = daily_fr.iter().sum::() / Decimal::from(daily_fr.len()); - println!("assigned_fr: {}", assigned_fr); + let assigned_fr = logger.execute( + &format!("Avg. failure rate for node: {}", node.node_id), + Operation::Avg(daily_fr.clone()), + ); avg_assigned_fr.push(assigned_fr); } } - let region_nodetype_rewards: HashMap = - base_rewards_region_nodetype(logger, ®ion_node_type_rewardables, rewards_table); - - let avg_assigned_fr_len = avg_assigned_fr.len(); - let unassigned_fr: Decimal = if avg_assigned_fr_len > 0 { - avg_assigned_fr.iter().sum::() / Decimal::from(avg_assigned_fr.len()) + let unassigned_fr: Decimal = if avg_assigned_fr.len() > 0 { + logger.execute( + "Unassigned days failure rate:", + Operation::Avg(avg_assigned_fr), + ) } else { dec!(1) }; - println!("unassigned_fr: {}", unassigned_fr); - let rewards_reduction_unassigned = rewards_reduction_percent(logger, &unassigned_fr); let multiplier_unassigned = logger.execute( - "Reward Multiplier Fully Unassigned Nodes", + "Reward multiplier fully unassigned nodes:", Operation::Subtract(dec!(1), rewards_reduction_unassigned), ); - println!("multiplier_unassigned: {}", multiplier_unassigned); - // 3. reward the nodes of node provider let mut sorted_rewardables = rewardables.to_vec(); sorted_rewardables.sort_by(|a, b| a.region.cmp(&b.region).then(a.node_type.cmp(&b.node_type))); for node in sorted_rewardables { + logger.add_entry( + LogLevel::High, + LogEntry::ComputeRewardsForNode { + node_id: node.node_id, + node_type: node.node_type.clone(), + region: node.region.clone(), + }, + ); + let node_type = node.node_type.clone(); let region = node.region.clone(); @@ -166,45 +200,59 @@ fn node_provider_rewards( .expect("Rewards already filled") }; - rewards_xdr_no_penalty_total.push(Operation::Multiply(*rewards_xdr_no_penalty, dec!(1))); + logger.add_entry( + LogLevel::Mid, + LogEntry::BaseRewards(*rewards_xdr_no_penalty), + ); + + rewards_xdr_no_penalty_total.push(*rewards_xdr_no_penalty); // Node Providers with less than 4 machines are rewarded fully, independently of their performance if rewardable_nodes_count < FULL_REWARDS_MACHINES_LIMIT { - rewards_xdr_total.push(Operation::Multiply(*rewards_xdr_no_penalty, dec!(1))); + rewards_xdr_total.push(*rewards_xdr_no_penalty); continue; } // In this case the node has been assigned to a subnet if let Some(mut daily_idiosyncratic_fr) = nodes_idiosyncratic_fr.remove(&node.node_id) { + logger.add_entry(LogLevel::Mid, LogEntry::NodeStatusAssigned); // resize the daily_idiosyncratic_fr to the number of days in the period daily_idiosyncratic_fr.resize(days_in_period as usize, unassigned_fr); - let multiplier_assigned = assigned_multiplier(logger, daily_idiosyncratic_fr); - - println!("rewards_xdr_no_penalty: {}", rewards_xdr_no_penalty); - println!("multiplier_assigned: {}", multiplier_assigned); + logger.add_entry( + LogLevel::Mid, + LogEntry::IdiosyncraticFailureRates(daily_idiosyncratic_fr.clone()), + ); - rewards_xdr_total.push(Operation::Multiply( - *rewards_xdr_no_penalty, - multiplier_assigned, - )); + let multiplier_assigned = assigned_multiplier(logger, daily_idiosyncratic_fr); + let rewards_xdr = logger.execute( + "Rewards XDR for the node", + Operation::Multiply(*rewards_xdr_no_penalty, multiplier_assigned), + ); + rewards_xdr_total.push(rewards_xdr); } else { - rewards_xdr_total.push(Operation::Multiply( - *rewards_xdr_no_penalty, - multiplier_unassigned, - )); + logger.add_entry(LogLevel::Mid, LogEntry::NodeStatusUnassigned); + + let rewards_xdr = logger.execute( + "Rewards XDR for the node", + Operation::Multiply(*rewards_xdr_no_penalty, multiplier_unassigned), + ); + rewards_xdr_total.push(rewards_xdr); } } let rewards_xdr_total = logger.execute( "Compute total permyriad XDR", - Operation::SumOps(rewards_xdr_total), + Operation::Sum(rewards_xdr_total), ); let rewards_xdr_no_reduction_total = logger.execute( "Compute total permyriad XDR no performance penalty", - Operation::SumOps(rewards_xdr_no_penalty_total), + Operation::Sum(rewards_xdr_no_penalty_total), + ); + logger.add_entry( + LogLevel::High, + LogEntry::RewardsXDRTotal(rewards_xdr_total, rewards_xdr_no_reduction_total), ); - logger.add_entry(LogEntry::RewardsXDRTotal(rewards_xdr_total)); Rewards { xdr_permyriad: rewards_xdr_total.to_u64().unwrap(), @@ -217,7 +265,7 @@ fn assigned_multiplier(logger: &mut RewardsLog, daily_failure_rate: Vec let rewards_reduction = rewards_reduction_percent(logger, &average_fr); logger.execute( - "Reward Multiplier Assigned", + "Reward Multiplier", Operation::Subtract(dec!(1), rewards_reduction), ) } @@ -343,18 +391,21 @@ fn rewards_reduction_percent(logger: &mut RewardsLog, failure_rate: &Decimal) -> Operation::Set(dec!(0.8)), ) } else { - let y_change = logger.execute( - "Linear Reduction Y change", - Operation::Subtract(*failure_rate, MIN_FAILURE_RATE), - ); - let x_change = logger.execute( - "Linear Reduction X change", - Operation::Subtract(MAX_FAILURE_RATE, MIN_FAILURE_RATE), + let rewards_reduction = (*failure_rate - MIN_FAILURE_RATE) + / (MAX_FAILURE_RATE - MIN_FAILURE_RATE) + * MAX_REWARDS_REDUCTION; + logger.add_entry( + LogLevel::Mid, + LogEntry::RewardsReductionPercent { + failure_rate: *failure_rate, + min_fr: MIN_FAILURE_RATE, + max_fr: MAX_FAILURE_RATE, + max_rr: MAX_REWARDS_REDUCTION, + rewards_reduction, + }, ); - let m = logger.execute("Compute m", Operation::Divide(y_change, x_change)); - - logger.execute(RF, Operation::Multiply(m, dec!(0.8))) + rewards_reduction } } @@ -386,10 +437,13 @@ fn base_rewards_region_nodetype( let rate = match rewards_table.get_rate(region, node_type) { Some(rate) => rate, None => { - logger.add_entry(LogEntry::RateNotFoundInRewardTable { - node_type: node_type.to_string(), - region: region.to_string(), - }); + logger.add_entry( + LogLevel::High, + LogEntry::RateNotFoundInRewardTable { + node_type: node_type.to_string(), + region: region.to_string(), + }, + ); NodeRewardRate { xdr_permyriad_per_node_per_month: 1, @@ -425,12 +479,16 @@ fn base_rewards_region_nodetype( region_nodetype_rewards.insert((region.clone(), node_type.clone()), base_rewards); } - logger.add_entry(LogEntry::RewardTableEntry { - node_type: node_type.to_string(), - region: region.to_string(), - coeff, - base_rewards, - }); + logger.add_entry( + LogLevel::Mid, + LogEntry::RewardTableEntry { + node_type: node_type.to_string(), + region: region.to_string(), + coeff, + base_rewards, + node_count: *node_count, + }, + ); } // Computes node rewards for type3* nodes in all regions and add it to region_nodetype_rewards @@ -454,13 +512,6 @@ fn base_rewards_region_nodetype( Operation::Divide(region_rewards, Decimal::from(rewards_len)), ); - logger.add_entry(LogEntry::AvgType3Rewards { - region: key.0.clone(), - rewards_avg, - coefficients_avg, - region_rewards_avg, - }); - region_nodetype_rewards.insert(key, region_rewards_avg); } diff --git a/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs index 201019cc176..cedfc55c982 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs @@ -1,9 +1,8 @@ +use super::*; use ic_protobuf::registry::node_rewards::v2::NodeRewardRates; use num_traits::FromPrimitive; use std::collections::BTreeMap; -use super::*; - #[derive(Clone)] struct MockedMetrics { days: u64, @@ -320,6 +319,7 @@ fn test_systematic_fr_calculation() { #[test] fn test_idiosyncratic_daily_fr_correct_values() { + let mut logger = RewardsLog::default(); let node1 = PrincipalId::new_user_test_id(1); let node2 = PrincipalId::new_user_test_id(2); let subnet1 = PrincipalId::new_user_test_id(10); @@ -345,7 +345,7 @@ fn test_idiosyncratic_daily_fr_correct_values() { ((subnet1, 3), dec!(0.1)), ]); - let result = idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); + let result = idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); let expected = HashMap::from([ (node1, vec![dec!(0.1), dec!(0.3), dec!(0.749)]), // (0.2 - 0.1), (0.5 - 0.2), (0.849 - 0.1) @@ -358,6 +358,7 @@ fn test_idiosyncratic_daily_fr_correct_values() { #[test] #[should_panic(expected = "Systematic failure rate not found")] fn test_idiosyncratic_daily_fr_missing_systematic_fr() { + let mut logger = RewardsLog::default(); let node1 = PrincipalId::new_user_test_id(1); let subnet1 = PrincipalId::new_user_test_id(10); @@ -368,11 +369,12 @@ fn test_idiosyncratic_daily_fr_missing_systematic_fr() { let subnets_systematic_fr = HashMap::from([((subnet1, 2), dec!(0.1))]); - idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); + idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); } #[test] fn test_idiosyncratic_daily_fr_negative_failure_rate() { + let mut logger = RewardsLog::default(); let node1 = PrincipalId::new_user_test_id(1); let subnet1 = PrincipalId::new_user_test_id(10); @@ -383,7 +385,7 @@ fn test_idiosyncratic_daily_fr_negative_failure_rate() { let subnets_systematic_fr = HashMap::from([((subnet1, 1), dec!(0.1))]); - let result = idiosyncratic_daily_fr(&assigned_metrics, &subnets_systematic_fr); + let result = idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); // Expecting zero due to saturation let expected = HashMap::from([(node1, vec![Decimal::ZERO])]); @@ -445,15 +447,6 @@ fn test_node_provider_below_min_limit() { assert_eq!(rewards.xdr_permyriad_no_reduction, 2); } -fn helper_dummy_rewardables(node_id: PrincipalId, node_provider_id: PrincipalId) -> RewardableNode { - RewardableNode { - node_id, - node_provider_id, - region: "A,B".to_string(), - node_type: "type1".to_string(), - } -} - #[test] fn test_node_provider_rewards_one_assigned() { let mut logger = RewardsLog::default(); @@ -461,11 +454,11 @@ fn test_node_provider_rewards_one_assigned() { let days_in_period = 30; let rewardables = (1..=5) - .map(|i| { - helper_dummy_rewardables( - PrincipalId::new_user_test_id(i), - PrincipalId::new_anonymous(), - ) + .map(|i| RewardableNode { + node_id: PrincipalId::new_user_test_id(i), + node_provider_id: PrincipalId::new_anonymous(), + region: "A,B".to_string(), + node_type: "type1".to_string(), }) .collect_vec(); @@ -483,9 +476,43 @@ fn test_node_provider_rewards_one_assigned() { &node_rewards_table, ); - // Unassigned failure rate: 0.325 - // Unassigned multiplier: 1 - (0.325-0.1) / (0.6-0.1) * 0.8 = 0.64 Rewards: 1000 * 0.64 = 640 XDRs - // Total rewards: 640 * 5 = 3200 XDRs + println!("{}", logger.get_log()); + + // Compute Base Rewards For RegionNodeType + // - node_type: type1, region: A,B, coeff: 1, base_rewards: 1000, node_count: 5 + // Compute Unassigned Days Failure Rate + // - Avg. failure rate for node: 6fyp7-3ibaa-aaaaa-aaaap-4ai: avg(0.4,0.2,0.3,0.4) = 0.325 + // - Unassigned days failure rate:: avg(0.325) = 0.325 + // - Rewards reduction percent: (0.325 - 0.1) / (0.6 - 0.1) * 0.8 = 0.360 + // - Reward multiplier fully unassigned nodes:: 1 - 0.360 = 0.640 + // Compute Rewards For Node | node_id=6fyp7-3ibaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Assigned + // - Idiosyncratic daily failure rates : 0.4,0.2,0.3,0.4,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325 + // - Failure rate average: avg(0.4,0.2,0.3,0.4,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325) = 0.325 + // - Rewards reduction percent: (0.325 - 0.1) / (0.6 - 0.1) * 0.8 = 0.360 + // - Reward Multiplier: 1 - 0.360 = 0.640 + // - Rewards XDR for the node: 1000 * 0.640 = 640.000 + // Compute Rewards For Node | node_id=djduj-3qcaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Unassigned + // - Rewards XDR for the node: 1000 * 0.640 = 640.000 + // Compute Rewards For Node | node_id=6wcs7-uadaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Unassigned + // - Rewards XDR for the node: 1000 * 0.640 = 640.000 + // Compute Rewards For Node | node_id=c5mtj-kieaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Unassigned + // - Rewards XDR for the node: 1000 * 0.640 = 640.000 + // Compute Rewards For Node | node_id=7cnv7-fyfaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Unassigned + // - Rewards XDR for the node: 1000 * 0.640 = 640.000 + // - Compute total permyriad XDR: sum(640.000,640.000,640.000,640.000,640.000) = 3200.000 + // - Compute total permyriad XDR no performance penalty: sum(1000,1000,1000,1000,1000) = 5000 + // Total rewards XDR permyriad: 3200.000 + // Total rewards XDR permyriad not adjusted: 5000 assert_eq!(rewards.xdr_permyriad, 3200); assert_eq!(rewards.xdr_permyriad_no_reduction, 5000); } @@ -497,11 +524,11 @@ fn test_node_provider_rewards_two_assigned() { let days_in_period = 30; let rewardables = (1..=5) - .map(|i| { - helper_dummy_rewardables( - PrincipalId::new_user_test_id(i), - PrincipalId::new_anonymous(), - ) + .map(|i| RewardableNode { + node_id: PrincipalId::new_user_test_id(i), + node_provider_id: PrincipalId::new_anonymous(), + region: "A,B".to_string(), + node_type: "type1".to_string(), }) .collect_vec(); @@ -523,150 +550,203 @@ fn test_node_provider_rewards_two_assigned() { &node_rewards_table, ); - // Avg. assigned failure rate: (0.325 + 0.4765) / 2 = 0.40075 - // 3 nodes are unassigned in the period: - // Unassigned failure rate: 0.40075 - // Unassigned multiplier: 1 - (0.40075-0.1) / (0.6-0.1) * 0.8 = 0.51880 - // Rewards: 1000 * 0.51880 = 518.80 XDRs - // 2 nodes are assigned in the period: - // node1: - // failure rate = (0.325 * 4 + 0.40075 * 26) / 30 = 0.390 - // multiplier = 1 - (0.390-0.1) / (0.6-0.1) * 0.8 = 0.53496 - // Rewards: 1000 * 0.53496 = 534.96 XDRs - // node2: - // failure rate = (0.4765 * 4 + 0.40075 * 26) / 30 = 0.41 - // multiplier = 1 - (0.41-0.1) / (0.6-0.1) * 0.8 = 0.50264 - // Rewards: 1000 * 0.50264 = 502.64 XDRs - // Total rewards: 518.80 * 3 + 534.96 + 502.64 = 2594 XDRs + // Compute Base Rewards For RegionNodeType + // - node_type: type1, region: A,B, coeff: 1, base_rewards: 1000, node_count: 5 + // Compute Unassigned Days Failure Rate + // - Avg. failure rate for node: 6fyp7-3ibaa-aaaaa-aaaap-4ai: avg(0.4,0.2,0.3,0.4) = 0.325 + // - Avg. failure rate for node: djduj-3qcaa-aaaaa-aaaap-4ai: avg(0.9,0.6,0.304,0.102) = 0.4765 + // - Unassigned days failure rate:: avg(0.325,0.4765) = 0.4008 + // - Rewards reduction percent: (0.4008 - 0.1) / (0.6 - 0.1) * 0.8 = 0.4812 + // - Reward multiplier fully unassigned nodes:: 1 - 0.4812 = 0.5188 + // Compute Rewards For Node | node_id=6fyp7-3ibaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Assigned + // - Idiosyncratic daily failure rates : 0.4,0.2,0.3,0.4,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075 + // - Failure rate average: avg(0.4,0.2,0.3,0.4,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008) = 0.3906 + // - Rewards reduction percent: (0.3906 - 0.1) / (0.6 - 0.1) * 0.8 = 0.4650 + // - Reward Multiplier: 1 - 0.4650 = 0.5350 + // - Rewards XDR for the node: 1000 * 0.5350 = 534.9600 + // Compute Rewards For Node | node_id=djduj-3qcaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Assigned + // - Idiosyncratic daily failure rates : 0.9,0.6,0.304,0.102,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075,0.40075 + // - Failure rate average: avg(0.9,0.6,0.304,0.102,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008,0.4008) = 0.4108 + // - Rewards reduction percent: (0.4108 - 0.1) / (0.6 - 0.1) * 0.8 = 0.4974 + // - Reward Multiplier: 1 - 0.4974 = 0.5026 + // - Rewards XDR for the node: 1000 * 0.5026 = 502.6400 + // Compute Rewards For Node | node_id=6wcs7-uadaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Unassigned + // - Rewards XDR for the node: 1000 * 0.5188 = 518.8000 + // Compute Rewards For Node | node_id=c5mtj-kieaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Unassigned + // - Rewards XDR for the node: 1000 * 0.5188 = 518.8000 + // Compute Rewards For Node | node_id=7cnv7-fyfaa-aaaaa-aaaap-4ai, node_type=type1, region=A,B + // - Base rewards XDRs: 1000 + // - Node status: Unassigned + // - Rewards XDR for the node: 1000 * 0.5188 = 518.8000 + // - Compute total permyriad XDR: sum(534.9600,502.6400,518.8000,518.8000,518.8000) = 2594.0000 + // - Compute total permyriad XDR no performance penalty: sum(1000,1000,1000,1000,1000) = 5000 + // Total rewards XDR permyriad: 2594.0000 + // Total rewards XDR permyriad not adjusted: 5000 + assert_eq!(rewards.xdr_permyriad, 2594); assert_eq!(rewards.xdr_permyriad_no_reduction, 5000); } -// #[test] -// fn test_np_rewards_type3_coeff() { -// let mut logger = RewardsLog::default(); -// let mut assigned_multipliers: HashMap> = -// HashMap::default(); -// let mut rewardable_nodes: HashMap = HashMap::default(); -// let region_node_type = ("A,B,C".to_string(), "type3.1".to_string()); -// -// // 4 nodes in period: 1 assigned, 3 unassigned -// rewardable_nodes.insert(region_node_type.clone(), 4); -// assigned_multipliers.insert(region_node_type, vec![dec!(0.5)]); -// let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); -// let rewards = node_provider_rewards( -// &mut logger, -// &assigned_multipliers, -// &rewardable_nodes, -// &node_rewards_table, -// ); -// -// let rewardables = vec![RewardableNode { -// node_id: PrincipalId::new_user_test_id(1), -// node_provider_id: PrincipalId::new_user_test_id(2), -// region: "A,B,C".to_string(), -// node_type: "type3.1".to_string(), -// }]; -// let mut assigned_metrics = HashMap::new(); -// assigned_metrics.insert( -// PrincipalId::new_user_test_id(1), -// vec![DailyNodeMetrics::new( -// 0, -// PrincipalId::new_user_test_id(1), -// 10, -// 1, -// )], -// ); -// let subnets_systematic_fr = HashMap::new(); -// let days_in_period = 30; -// let rewards_table = NodeRewardsTable::default(); -// -// let rewards = node_provider_rewards( -// &mut logger, -// &rewardables, -// &assigned_metrics, -// &subnets_systematic_fr, -// days_in_period, -// &rewards_table, -// ); -// -// // Coefficients avg., operation=avg(0.95,0.95,0.95,0.95), result=0.95 -// // Rewards avg., operation=avg(1500,1500,1500,1500), result=1500 -// // Total rewards after coefficient reduction, operation=sum(1500 * 1,1500 * 0.95,1500 * 0.9025,1500 * 0.8574), result=5564 -// -// // Rewards average after coefficient reduction, operation=5564 / 4, result=1391 -// // Total XDR no penalties, operation=sum(1391,1391,1391,1391), result=5564 -// assert_eq!(rewards.xdr_permyriad_no_reduction, 5564); -// -// // Total XDR, operation=sum(1391 * 0.5,1391 * 0.5,1391 * 0.5,1391 * 0.5), result=2782 -// assert_eq!(rewards.xdr_permyriad, 2782); -// } - -// #[test] -// fn test_np_rewards_type3_coeff() { -// let mut logger = RewardsLog::default(); -// let mut assigned_multipliers: HashMap> = -// HashMap::default(); -// let mut rewardable_nodes: HashMap = HashMap::default(); -// let region_node_type = ("A,B,C".to_string(), "type3.1".to_string()); -// -// // 4 nodes in period: 1 assigned, 3 unassigned -// rewardable_nodes.insert(region_node_type.clone(), 4); -// assigned_multipliers.insert(region_node_type, vec![dec!(0.5)]); -// let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); -// let rewards = node_provider_rewards( -// &mut logger, -// &assigned_multipliers, -// &rewardable_nodes, -// &node_rewards_table, -// ); -// -// // Coefficients avg., operation=avg(0.95,0.95,0.95,0.95), result=0.95 -// // Rewards avg., operation=avg(1500,1500,1500,1500), result=1500 -// // Total rewards after coefficient reduction, operation=sum(1500 * 1,1500 * 0.95,1500 * 0.9025,1500 * 0.8574), result=5564 -// -// // Rewards average after coefficient reduction, operation=5564 / 4, result=1391 -// // Total XDR no penalties, operation=sum(1391,1391,1391,1391), result=5564 -// assert_eq!(rewards.xdr_permyriad_no_reduction, 5564); -// -// // Total XDR, operation=sum(1391 * 0.5,1391 * 0.5,1391 * 0.5,1391 * 0.5), result=2782 -// assert_eq!(rewards.xdr_permyriad, 2782); -// } - -// #[test] -// fn test_np_rewards_type3_mix() { -// let mut logger = RewardsLog::default(); -// let mut assigned_multipliers: HashMap> = -// HashMap::default(); -// let mut rewardable_nodes: HashMap = HashMap::default(); -// -// // 5 nodes in period: 2 assigned, 3 unassigned -// assigned_multipliers.insert( -// ("A,B,D".to_string(), "type3".to_string()), -// vec![dec!(0.5), dec!(0.4)], -// ); -// -// // This will take rates from outer -// rewardable_nodes.insert(("A,B,D".to_string(), "type3".to_string()), 3); -// -// // This will take rates from inner -// rewardable_nodes.insert(("A,B,C".to_string(), "type3.1".to_string()), 2); -// -// let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); -// let rewards = node_provider_rewards( -// &mut logger, -// &assigned_multipliers, -// &rewardable_nodes, -// &node_rewards_table, -// ); -// -// // Coefficients avg(0.95,0.95,0.97,0.97,0.97) = 0.9620 -// // Rewards avg., operation=avg(1500,1500,1000,1000,1000), result=1200 -// // Rewards average sum(1200 * 1,1200 * 0.9620,1200 * 0.9254,1200 * 0.8903,1200 * 0.8564) / 5, result=1112 -// // Unassigned Nodes Multiplier, operation=avg(0.5,0.4), result=0.450 -// -// // Total XDR, operation=sum(1112 * 0.450,1112 * 0.450,1112 * 0.5,1112 * 0.4,1112 * 0.450), result=2502 -// assert_eq!(rewards.xdr_permyriad, 2502); -// // Total XDR no penalties, operation=1112 * 5, result=5561 -// assert_eq!(rewards.xdr_permyriad_no_reduction, 5561); -// } +#[test] +fn test_np_rewards_type3_coeff() { + let mut logger = RewardsLog::default(); + let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); + let days_in_period = 30; + + // 4 nodes in period: 1 assigned, 3 unassigned + let rewardables = (1..=4) + .map(|i| RewardableNode { + node_id: PrincipalId::new_user_test_id(i), + node_provider_id: PrincipalId::new_anonymous(), + region: "A,B,C".to_string(), + node_type: "type3.1".to_string(), + }) + .collect_vec(); + let mut nodes_idiosyncratic_fr = HashMap::new(); + nodes_idiosyncratic_fr.insert( + PrincipalId::new_user_test_id(1), + vec![dec!(0.4), dec!(0.2), dec!(0.3), dec!(0.4)], // Avg. 0.325 + ); + + let rewards = node_provider_rewards( + &mut logger, + &rewardables, + nodes_idiosyncratic_fr, + days_in_period, + &node_rewards_table, + ); + + // Compute Base Rewards For RegionNodeType + // - node_type: type3.1, region: A,B,C, coeff: 0.95, base_rewards: 1500, node_count: 4 + // - Coefficients avg.: avg(0.95,0.95,0.95,0.95) = 0.95 + // - Rewards avg.: avg(1500,1500,1500,1500) = 1500 + // - Total rewards after coefficient reduction: sum(1500 * 1,1500 * 0.95,1500 * 0.9025,1500 * 0.8574) = 5564.8125 + // - Rewards average after coefficient reduction: 5564.8125 / 4 = 1391.2031 + // Compute Unassigned Days Failure Rate + // - Avg. failure rate for node: 6fyp7-3ibaa-aaaaa-aaaap-4ai: avg(0.4,0.2,0.3,0.4) = 0.325 + // - Unassigned days failure rate:: avg(0.325) = 0.325 + // - Rewards reduction percent: (0.325 - 0.1) / (0.6 - 0.1) * 0.8 = 0.360 + // - Reward multiplier fully unassigned nodes:: 1 - 0.360 = 0.640 + // Compute Rewards For Node | node_id=6fyp7-3ibaa-aaaaa-aaaap-4ai, node_type=type3.1, region=A,B,C + // - Base rewards XDRs: 1391.2031 + // - Node status: Assigned + // - Idiosyncratic daily failure rates : 0.4,0.2,0.3,0.4,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325 + // - Failure rate average: avg(0.4,0.2,0.3,0.4,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325,0.325) = 0.325 + // - Rewards reduction percent: (0.325 - 0.1) / (0.6 - 0.1) * 0.8 = 0.360 + // - Reward Multiplier: 1 - 0.360 = 0.640 + // - Rewards XDR for the node: 1391.2031 * 0.640 = 890.3700 + // Compute Rewards For Node | node_id=djduj-3qcaa-aaaaa-aaaap-4ai, node_type=type3.1, region=A,B,C + // - Base rewards XDRs: 1391.2031 + // - Node status: Unassigned + // - Rewards XDR for the node: 1391.2031 * 0.640 = 890.3700 + // Compute Rewards For Node | node_id=6wcs7-uadaa-aaaaa-aaaap-4ai, node_type=type3.1, region=A,B,C + // - Base rewards XDRs: 1391.2031 + // - Node status: Unassigned + // - Rewards XDR for the node: 1391.2031 * 0.640 = 890.3700 + // Compute Rewards For Node | node_id=c5mtj-kieaa-aaaaa-aaaap-4ai, node_type=type3.1, region=A,B,C + // - Base rewards XDRs: 1391.2031 + // - Node status: Unassigned + // - Rewards XDR for the node: 1391.2031 * 0.640 = 890.3700 + // - Compute total permyriad XDR: sum(890.3700,890.3700,890.3700,890.3700) = 3561.4800 + // - Compute total permyriad XDR no performance penalty: sum(1391.2031,1391.2031,1391.2031,1391.2031) = 5564.8125 + // Total rewards XDR permyriad: 3561.4800 + // Total rewards XDR permyriad not adjusted: 5564.8125 + + assert_eq!(rewards.xdr_permyriad, 3561); + assert_eq!(rewards.xdr_permyriad_no_reduction, 5564); +} + +#[test] +fn test_np_rewards_type3_mix() { + let mut logger = RewardsLog::default(); + let node_rewards_table: NodeRewardsTable = mocked_rewards_table(); + let days_in_period = 30; + + // 4 nodes in period: 1 assigned, 3 unassigned + let mut rewardables = (1..=3) + .map(|i| RewardableNode { + node_id: PrincipalId::new_user_test_id(i), + node_provider_id: PrincipalId::new_anonymous(), + region: "A,B,C".to_string(), + node_type: "type3.1".to_string(), + }) + .collect_vec(); + + rewardables.push(RewardableNode { + node_id: PrincipalId::new_user_test_id(4), + node_provider_id: PrincipalId::new_anonymous(), + region: "A,B,D".to_string(), + node_type: "type3".to_string(), + }); + + let mut nodes_idiosyncratic_fr = HashMap::new(); + nodes_idiosyncratic_fr.insert( + PrincipalId::new_user_test_id(3), + vec![dec!(0.1), dec!(0.12), dec!(0.23), dec!(0.12)], + ); + nodes_idiosyncratic_fr.insert( + PrincipalId::new_user_test_id(4), + vec![dec!(0.2), dec!(0.32), dec!(0.123), dec!(0.432)], + ); + + let rewards = node_provider_rewards( + &mut logger, + &rewardables, + nodes_idiosyncratic_fr, + days_in_period, + &node_rewards_table, + ); + + // Compute Base Rewards For RegionNodeType + // - node_type: type3, region: A,B,D, coeff: 0.97, base_rewards: 1000, node_count: 1 + // - node_type: type3.1, region: A,B,C, coeff: 0.95, base_rewards: 1500, node_count: 3 + // - Coefficients avg.: avg(0.97,0.95,0.95,0.95) = 0.9550 + // - Rewards avg.: avg(1000,1500,1500,1500) = 1375 + // - Total rewards after coefficient reduction: sum(1375 * 1,1375 * 0.9550,1375 * 0.9120,1375 * 0.8710) = 5139.7622 + // - Rewards average after coefficient reduction: 5139.7622 / 4 = 1284.9406 + // Compute Unassigned Days Failure Rate + // - Avg. failure rate for node: 6wcs7-uadaa-aaaaa-aaaap-4ai: avg(0.1,0.12,0.23,0.12) = 0.1425 + // - Avg. failure rate for node: c5mtj-kieaa-aaaaa-aaaap-4ai: avg(0.2,0.32,0.123,0.432) = 0.2688 + // - Unassigned days failure rate:: avg(0.1425,0.2688) = 0.2056 + // - Rewards reduction percent: (0.2056 - 0.1) / (0.6 - 0.1) * 0.8 = 0.1690 + // - Reward multiplier fully unassigned nodes:: 1 - 0.1690 = 0.8310 + // Compute Rewards For Node | node_id=6fyp7-3ibaa-aaaaa-aaaap-4ai, node_type=type3.1, region=A,B,C + // - Base rewards XDRs: 1284.9406 + // - Node status: Unassigned + // - Rewards XDR for the node: 1284.9406 * 0.8310 = 1067.7856 + // Compute Rewards For Node | node_id=djduj-3qcaa-aaaaa-aaaap-4ai, node_type=type3.1, region=A,B,C + // - Base rewards XDRs: 1284.9406 + // - Node status: Unassigned + // - Rewards XDR for the node: 1284.9406 * 0.8310 = 1067.7856 + // Compute Rewards For Node | node_id=6wcs7-uadaa-aaaaa-aaaap-4ai, node_type=type3.1, region=A,B,C + // - Base rewards XDRs: 1284.9406 + // - Node status: Assigned + // - Idiosyncratic daily failure rates : 0.1,0.12,0.23,0.12,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250 + // - Failure rate average: avg(0.1,0.12,0.23,0.12,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056) = 0.1972 + // - Rewards reduction percent: (0.1972 - 0.1) / (0.6 - 0.1) * 0.8 = 0.1555 + // - Reward Multiplier: 1 - 0.1555 = 0.8445 + // - Rewards XDR for the node: 1284.9406 * 0.8445 = 1085.0895 + // Compute Rewards For Node | node_id=c5mtj-kieaa-aaaaa-aaaap-4ai, node_type=type3, region=A,B,D + // - Base rewards XDRs: 1284.9406 + // - Node status: Assigned + // - Idiosyncratic daily failure rates : 0.2,0.32,0.123,0.432,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250,0.2056250 + // - Failure rate average: avg(0.2,0.32,0.123,0.432,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056,0.2056) = 0.2140 + // - Rewards reduction percent: (0.2140 - 0.1) / (0.6 - 0.1) * 0.8 = 0.1825 + // - Reward Multiplier: 1 - 0.1825 = 0.8175 + // - Rewards XDR for the node: 1284.9406 * 0.8175 = 1050.4817 + // - Compute total permyriad XDR: sum(1067.7856,1067.7856,1085.0895,1050.4817) = 4271.1424 + // - Compute total permyriad XDR no performance penalty: sum(1284.9406,1284.9406,1284.9406,1284.9406) = 5139.7622 + // Total rewards XDR permyriad: 4271.1424 + // Total rewards XDR permyriad not adjusted: 5139.7622 + + assert_eq!(rewards.xdr_permyriad, 4271); + assert_eq!(rewards.xdr_permyriad_no_reduction, 5139); +} diff --git a/rs/registry/node_provider_rewards/src/v1_types.rs b/rs/registry/node_provider_rewards/src/v1_types.rs index e96fe216f2a..bcab1a10244 100644 --- a/rs/registry/node_provider_rewards/src/v1_types.rs +++ b/rs/registry/node_provider_rewards/src/v1_types.rs @@ -1,11 +1,11 @@ -use std::collections::HashMap; - use candid::CandidType; use ic_base_types::PrincipalId; use ic_management_canister_types::NodeMetricsHistoryResponse; use num_traits::FromPrimitive; use rust_decimal::Decimal; use serde::Deserialize; +use std::collections::HashMap; +use std::fmt; use crate::v1_logs::RewardsLog; @@ -32,6 +32,16 @@ pub struct DailyNodeMetrics { pub failure_rate: Decimal, } +impl fmt::Display for DailyNodeMetrics { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "num_blocks_proposed: {}, num_blocks_failed: {}, failure_rate: {}", + self.num_blocks_proposed, self.num_blocks_failed, self.failure_rate + ) + } +} + impl DailyNodeMetrics { pub fn new( ts: u64, From b1ed7763dda6466aef99199547a6d6f7d7b2746f Mon Sep 17 00:00:00 2001 From: Pietro Date: Tue, 10 Dec 2024 15:38:39 +0100 Subject: [PATCH 12/20] Fix typos --- .../node_provider_rewards/src/v1_rewards.rs | 86 ++++++++++--------- .../src/v1_rewards/tests.rs | 8 +- .../node_provider_rewards/src/v1_types.rs | 15 ---- 3 files changed, 48 insertions(+), 61 deletions(-) diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index 87b26afc705..eae33ab4fea 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -16,19 +16,12 @@ use rust_decimal::Decimal; use rust_decimal_macros::dec; use std::collections::HashMap; -const FULL_REWARDS_MACHINES_LIMIT: u32 = 4; +const FULL_REWARDS_MACHINES_LIMIT: u32 = 3; const MIN_FAILURE_RATE: Decimal = dec!(0.1); const MAX_FAILURE_RATE: Decimal = dec!(0.6); const MAX_REWARDS_REDUCTION: Decimal = dec!(0.8); const RF: &str = "Linear Reduction factor"; -// The algo works as follows: -// 1. compute systematics failure rate of the subnets for each day currently 75percentile -// 2. derive idiosyncratic_daily_fr for each node in the subnet -// 3. compute the avg idiosyncratic failure rate to be assigned to each active period of the node -// 4. compute the avg idiosyncratic failure rates across all nodes for the node provider to be used in unassigned periods -// 5. for each node compute rewards multiplier -// 6. multiply per base rewards pub fn calculate_rewards( days_in_period: u64, rewards_table: &NodeRewardsTable, @@ -37,14 +30,13 @@ pub fn calculate_rewards( ) -> RewardsPerNodeProvider { let mut rewards_per_node_provider = HashMap::default(); let mut rewards_log_per_node_provider = HashMap::default(); - let mut all_assigned_metrics = daily_node_metrics(subnet_metrics); + let mut all_assigned_metrics = daily_node_metrics(subnet_metrics); let subnets_systematic_fr = systematic_fr_per_subnet(&all_assigned_metrics); let node_provider_rewardables = rewardables_by_node_provider(rewardable_nodes); for (node_provider_id, node_provider_rewardables) in node_provider_rewardables { let mut logger = RewardsLog::default(); - logger.add_entry( LogLevel::High, LogEntry::CalculateRewardsForNodeProvider(node_provider_id), @@ -59,13 +51,13 @@ pub fn calculate_rewards( .map(|daily_metrics| (node.node_id, daily_metrics)) }) .collect::>>(); - let idiosyncratic_daily_fr = - idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + let node_daily_fr = + nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); let rewards = node_provider_rewards( &mut logger, &node_provider_rewardables, - idiosyncratic_daily_fr, + node_daily_fr, days_in_period, rewards_table, ); @@ -80,7 +72,13 @@ pub fn calculate_rewards( } } -fn idiosyncratic_daily_fr( +/// Computes the idiosyncratic daily failure rates for each node. +/// +/// This function calculates the idiosyncratic failure rates by subtracting the systematic +/// failure rate of the subnet from the node's failure rate for each day. +/// If the node's failure rate is less than the systematic failure rate, the idiosyncratic +/// failure rate is set to zero. +fn nodes_idiosyncratic_fr( logger: &mut RewardsLog, assigned_metrics: &HashMap>, subnets_systematic_fr: &HashMap<(PrincipalId, TimestampNanos), Decimal>, @@ -93,7 +91,13 @@ fn idiosyncratic_daily_fr( for metrics in daily_metrics { let systematic_fr = subnets_systematic_fr .get(&(metrics.subnet_assigned, metrics.ts)) - .expect("Systematic failure rate not found"); + .expect( + format!( + "Systematic failure rate not found for subnet: {} and ts: {}", + metrics.subnet_assigned, metrics.ts + ) + .as_str(), + ); let fr = if metrics.failure_rate < *systematic_fr { Decimal::ZERO } else { @@ -124,7 +128,7 @@ fn node_provider_rewards( let mut rewards_xdr_total = Vec::new(); let mut rewards_xdr_no_penalty_total = Vec::new(); - let mut avg_assigned_fr: Vec = Vec::new(); + let mut nodes_active_fr: Vec = Vec::new(); let mut region_node_type_rewardables = HashMap::new(); let rewardable_nodes_count = rewardables.len() as u32; @@ -136,7 +140,6 @@ fn node_provider_rewards( LogEntry::ComputeBaseRewardsForRegionNodeType, ); for node in rewardables { - // Count the number of nodes per region and node type let nodes_count = region_node_type_rewardables .entry((node.region.clone(), node.node_type.clone())) .or_default(); @@ -145,28 +148,27 @@ fn node_provider_rewards( let region_nodetype_rewards: HashMap = base_rewards_region_nodetype(logger, ®ion_node_type_rewardables, rewards_table); - // 1. Compute the unassigned failure rate + // 1. Extrapolate the unassigned daily failure rate from the active nodes logger.add_entry(LogLevel::High, LogEntry::ComputeUnassignedFailureRate); for node in rewardables { - // 1. Compute the unassigned failure rate - if let Some(daily_fr) = nodes_idiosyncratic_fr.get(&node.node_id) { - let assigned_fr = logger.execute( + if let Some(fr) = nodes_idiosyncratic_fr.get(&node.node_id) { + let avg_fr = logger.execute( &format!("Avg. failure rate for node: {}", node.node_id), - Operation::Avg(daily_fr.clone()), + Operation::Avg(fr.clone()), ); - avg_assigned_fr.push(assigned_fr); + nodes_active_fr.push(avg_fr); } } - - let unassigned_fr: Decimal = if avg_assigned_fr.len() > 0 { + let unassigned_fr: Decimal = if nodes_active_fr.len() > 0 { logger.execute( "Unassigned days failure rate:", - Operation::Avg(avg_assigned_fr), + Operation::Avg(nodes_active_fr), ) } else { dec!(1) }; + // 2. Compute rewards multiplier for fully unassigned nodes let rewards_reduction_unassigned = rewards_reduction_percent(logger, &unassigned_fr); let multiplier_unassigned = logger.execute( "Reward multiplier fully unassigned nodes:", @@ -208,15 +210,15 @@ fn node_provider_rewards( rewards_xdr_no_penalty_total.push(*rewards_xdr_no_penalty); // Node Providers with less than 4 machines are rewarded fully, independently of their performance - if rewardable_nodes_count < FULL_REWARDS_MACHINES_LIMIT { + if rewardable_nodes_count <= FULL_REWARDS_MACHINES_LIMIT { rewards_xdr_total.push(*rewards_xdr_no_penalty); continue; } - // In this case the node has been assigned to a subnet - if let Some(mut daily_idiosyncratic_fr) = nodes_idiosyncratic_fr.remove(&node.node_id) { + let reward_multiplier = if let Some(mut daily_idiosyncratic_fr) = + nodes_idiosyncratic_fr.remove(&node.node_id) + { logger.add_entry(LogLevel::Mid, LogEntry::NodeStatusAssigned); - // resize the daily_idiosyncratic_fr to the number of days in the period daily_idiosyncratic_fr.resize(days_in_period as usize, unassigned_fr); logger.add_entry( @@ -225,20 +227,18 @@ fn node_provider_rewards( ); let multiplier_assigned = assigned_multiplier(logger, daily_idiosyncratic_fr); - let rewards_xdr = logger.execute( - "Rewards XDR for the node", - Operation::Multiply(*rewards_xdr_no_penalty, multiplier_assigned), - ); - rewards_xdr_total.push(rewards_xdr); + multiplier_assigned } else { logger.add_entry(LogLevel::Mid, LogEntry::NodeStatusUnassigned); - let rewards_xdr = logger.execute( - "Rewards XDR for the node", - Operation::Multiply(*rewards_xdr_no_penalty, multiplier_unassigned), - ); - rewards_xdr_total.push(rewards_xdr); - } + multiplier_unassigned + }; + + let rewards_xdr = logger.execute( + "Rewards XDR for the node", + Operation::Multiply(*rewards_xdr_no_penalty, reward_multiplier), + ); + rewards_xdr_total.push(rewards_xdr); } let rewards_xdr_total = logger.execute( @@ -270,6 +270,10 @@ fn assigned_multiplier(logger: &mut RewardsLog, daily_failure_rate: Vec ) } +/// Computes the systematic failure rate for each subnet per day. +/// +/// This function calculates the 75th percentile of failure rates for each subnet on a daily basis. +/// This represents the systematic failure rate for all the nodes in the subnet for that day. fn systematic_fr_per_subnet( daily_node_metrics: &HashMap>, ) -> HashMap<(PrincipalId, TimestampNanos), Decimal> { diff --git a/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs index cedfc55c982..c8aec2de01c 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs @@ -345,7 +345,7 @@ fn test_idiosyncratic_daily_fr_correct_values() { ((subnet1, 3), dec!(0.1)), ]); - let result = idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + let result = nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); let expected = HashMap::from([ (node1, vec![dec!(0.1), dec!(0.3), dec!(0.749)]), // (0.2 - 0.1), (0.5 - 0.2), (0.849 - 0.1) @@ -369,7 +369,7 @@ fn test_idiosyncratic_daily_fr_missing_systematic_fr() { let subnets_systematic_fr = HashMap::from([((subnet1, 2), dec!(0.1))]); - idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); } #[test] @@ -385,7 +385,7 @@ fn test_idiosyncratic_daily_fr_negative_failure_rate() { let subnets_systematic_fr = HashMap::from([((subnet1, 1), dec!(0.1))]); - let result = idiosyncratic_daily_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + let result = nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); // Expecting zero due to saturation let expected = HashMap::from([(node1, vec![Decimal::ZERO])]); @@ -476,8 +476,6 @@ fn test_node_provider_rewards_one_assigned() { &node_rewards_table, ); - println!("{}", logger.get_log()); - // Compute Base Rewards For RegionNodeType // - node_type: type1, region: A,B, coeff: 1, base_rewards: 1000, node_count: 5 // Compute Unassigned Days Failure Rate diff --git a/rs/registry/node_provider_rewards/src/v1_types.rs b/rs/registry/node_provider_rewards/src/v1_types.rs index bcab1a10244..9490aa30068 100644 --- a/rs/registry/node_provider_rewards/src/v1_types.rs +++ b/rs/registry/node_provider_rewards/src/v1_types.rs @@ -1,18 +1,14 @@ -use candid::CandidType; use ic_base_types::PrincipalId; use ic_management_canister_types::NodeMetricsHistoryResponse; use num_traits::FromPrimitive; use rust_decimal::Decimal; -use serde::Deserialize; use std::collections::HashMap; use std::fmt; use crate::v1_logs::RewardsLog; -pub type NodeMultiplierStats = (PrincipalId, MultiplierStats); pub type RegionNodeTypeCategory = (String, String); pub type TimestampNanos = u64; - pub type SubnetMetricsHistory = (PrincipalId, Vec); #[derive(Clone, Hash, Eq, PartialEq)] @@ -65,17 +61,6 @@ impl DailyNodeMetrics { } } -#[derive(Debug, Clone, Deserialize, CandidType)] -pub struct MultiplierStats { - pub days_assigned: u64, - pub days_unassigned: u64, - pub rewards_reduction: f64, - pub blocks_failed: u64, - pub blocks_proposed: u64, - pub blocks_total: u64, - pub failure_rate: f64, -} - pub struct RewardsPerNodeProvider { pub rewards_per_node_provider: HashMap, pub rewards_log_per_node_provider: HashMap, From 8bca036d468b19434ebced1bcaf9e309feee3bb2 Mon Sep 17 00:00:00 2001 From: Pietro Date: Tue, 10 Dec 2024 15:40:47 +0100 Subject: [PATCH 13/20] Remove unused dependencies --- Cargo.lock | 3 --- rs/registry/node_provider_rewards/Cargo.toml | 3 --- 2 files changed, 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 6abae96438c..f2c6ad557cf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11158,16 +11158,13 @@ dependencies = [ name = "ic-registry-node-provider-rewards" version = "0.9.0" dependencies = [ - "candid", "ic-base-types", "ic-management-canister-types", "ic-protobuf", "itertools 0.12.1", - "lazy_static", "num-traits", "rust_decimal", "rust_decimal_macros", - "serde", ] [[package]] diff --git a/rs/registry/node_provider_rewards/Cargo.toml b/rs/registry/node_provider_rewards/Cargo.toml index 4f784c384d1..6cfdfba548e 100644 --- a/rs/registry/node_provider_rewards/Cargo.toml +++ b/rs/registry/node_provider_rewards/Cargo.toml @@ -10,10 +10,7 @@ edition.workspace = true ic-base-types = { path = "../../types/base_types" } ic-protobuf = { path = "../../protobuf" } itertools = { workspace = true } -lazy_static = { workspace = true } num-traits = { workspace = true } rust_decimal = "1.36.0" rust_decimal_macros = "1.36.0" ic-management-canister-types = { path = "../../types/management_canister_types" } -serde = { workspace = true } -candid = { workspace = true } From dc7ea10a004a757cf1906a588b66b9a512cf8561 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Tue, 10 Dec 2024 14:59:03 +0000 Subject: [PATCH 14/20] Automatically updated Cargo*.lock --- Cargo.Bazel.Fuzzing.json.lock | 159 ++++++++++++++++++++++++++++++---- Cargo.Bazel.json.lock | 159 ++++++++++++++++++++++++++++++---- 2 files changed, 288 insertions(+), 30 deletions(-) diff --git a/Cargo.Bazel.Fuzzing.json.lock b/Cargo.Bazel.Fuzzing.json.lock index f2147abd496..24e011f9192 100644 --- a/Cargo.Bazel.Fuzzing.json.lock +++ b/Cargo.Bazel.Fuzzing.json.lock @@ -1,5 +1,5 @@ { - "checksum": "450ee9a1af0058d25f3cc1bc2b3906b3e4271ca79174522e2e1a71c0d9a9c85a", + "checksum": "ca7d9aae6f59918c467b580c8e5e3c11b0d2de7ca68fa4709ec0bf86530853be", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -1465,6 +1465,8 @@ ], "crate_features": { "common": [ + "compile-time-rng", + "const-random", "default", "getrandom", "runtime-rng", @@ -1482,6 +1484,10 @@ "id": "cfg-if 1.0.0", "target": "cfg_if" }, + { + "id": "const-random 0.1.18", + "target": "const_random" + }, { "id": "getrandom 0.2.10", "target": "getrandom" @@ -13703,6 +13709,110 @@ ], "license_file": "LICENSE-APACHE" }, + "const-random 0.1.18": { + "name": "const-random", + "version": "0.1.18", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random/0.1.18/download", + "sha256": "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" + } + }, + "targets": [ + { + "Library": { + "crate_name": "const_random", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "const-random-macro 0.1.16", + "target": "const_random_macro" + } + ], + "selects": {} + }, + "version": "0.1.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "const-random-macro 0.1.16": { + "name": "const-random-macro", + "version": "0.1.16", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random-macro/0.1.16/download", + "sha256": "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "const_random_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "getrandom 0.2.10", + "target": "getrandom" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "tiny-keccak 2.0.2", + "target": "tiny_keccak" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.16" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "convert_case 0.4.0": { "name": "convert_case", "version": "0.4.0", @@ -18419,6 +18529,10 @@ "id": "addr 0.15.6", "target": "addr" }, + { + "id": "ahash 0.8.11", + "target": "ahash" + }, { "id": "aide 0.13.4", "target": "aide" @@ -72954,46 +73068,60 @@ ], "selects": { "aarch64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "aarch64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ], "arm-unknown-linux-gnueabi": [ - "sha3" + "sha3", + "shake" ], "i686-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "i686-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "powerpc-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "s390x-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "x86_64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-freebsd": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ] } }, @@ -87634,6 +87762,7 @@ "actix-rt 2.10.0", "actix-web 4.9.0", "addr 0.15.6", + "ahash 0.8.11", "aide 0.13.4", "anyhow 1.0.93", "arbitrary 1.3.2", diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index d0949c64615..ae1aefb9656 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "b344eca479cfedf88abc9bad22dc5e3b5058749d7e97869732d8137396906468", + "checksum": "b6300b08e70fff1f06a92c5709d9c9a29a8c9b020a3b6f480eeffaea50968844", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -1469,6 +1469,8 @@ ], "crate_features": { "common": [ + "compile-time-rng", + "const-random", "default", "getrandom", "runtime-rng", @@ -1486,6 +1488,10 @@ "id": "cfg-if 1.0.0", "target": "cfg_if" }, + { + "id": "const-random 0.1.18", + "target": "const_random" + }, { "id": "getrandom 0.2.10", "target": "getrandom" @@ -13531,6 +13537,110 @@ ], "license_file": "LICENSE-APACHE" }, + "const-random 0.1.18": { + "name": "const-random", + "version": "0.1.18", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random/0.1.18/download", + "sha256": "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" + } + }, + "targets": [ + { + "Library": { + "crate_name": "const_random", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "const-random-macro 0.1.16", + "target": "const_random_macro" + } + ], + "selects": {} + }, + "version": "0.1.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "const-random-macro 0.1.16": { + "name": "const-random-macro", + "version": "0.1.16", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random-macro/0.1.16/download", + "sha256": "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "const_random_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "getrandom 0.2.10", + "target": "getrandom" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "tiny-keccak 2.0.2", + "target": "tiny_keccak" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.16" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "convert_case 0.4.0": { "name": "convert_case", "version": "0.4.0", @@ -18247,6 +18357,10 @@ "id": "addr 0.15.6", "target": "addr" }, + { + "id": "ahash 0.8.11", + "target": "ahash" + }, { "id": "aide 0.13.4", "target": "aide" @@ -72800,46 +72914,60 @@ ], "selects": { "aarch64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "aarch64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ], "arm-unknown-linux-gnueabi": [ - "sha3" + "sha3", + "shake" ], "i686-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "i686-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "powerpc-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "s390x-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "x86_64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-freebsd": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ] } }, @@ -87514,6 +87642,7 @@ "actix-rt 2.10.0", "actix-web 4.9.0", "addr 0.15.6", + "ahash 0.8.11", "aide 0.13.4", "anyhow 1.0.93", "arbitrary 1.3.2", From a6ad2cc5929e2dc628e82b84b29fd4572417f5b5 Mon Sep 17 00:00:00 2001 From: Pietro Date: Tue, 10 Dec 2024 17:37:54 +0100 Subject: [PATCH 15/20] Fixed clippy --- .../node_provider_rewards/src/v1_logs.rs | 12 +++--------- .../node_provider_rewards/src/v1_rewards.rs | 17 +++++------------ 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/rs/registry/node_provider_rewards/src/v1_logs.rs b/rs/registry/node_provider_rewards/src/v1_logs.rs index ecc8b7e80a6..6fda6aa8273 100644 --- a/rs/registry/node_provider_rewards/src/v1_logs.rs +++ b/rs/registry/node_provider_rewards/src/v1_logs.rs @@ -63,10 +63,7 @@ impl fmt::Display for Operation { return write!( f, "{}", - Operation::format_values( - &values.iter().map(|v| round_dp_4(v)).collect_vec(), - "sum" - ) + Operation::format_values(&values.iter().map(round_dp_4).collect_vec(), "sum") ) } Operation::SumOps(operations) => { @@ -76,10 +73,7 @@ impl fmt::Display for Operation { return write!( f, "{}", - Operation::format_values( - &values.iter().map(|v| round_dp_4(v)).collect_vec(), - "avg" - ) + Operation::format_values(&values.iter().map(round_dp_4).collect_vec(), "avg") ) } Operation::Subtract(o1, o2) => ("-", o1, o2), @@ -210,7 +204,7 @@ impl fmt::Display for LogEntry { write!( f, "Idiosyncratic daily failure rates : {}", - failure_rates.iter().map(|dec| dec).join(",") + failure_rates.iter().join(",") ) } LogEntry::RewardsReductionPercent { diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index eae33ab4fea..38e3f9e539a 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -91,13 +91,7 @@ fn nodes_idiosyncratic_fr( for metrics in daily_metrics { let systematic_fr = subnets_systematic_fr .get(&(metrics.subnet_assigned, metrics.ts)) - .expect( - format!( - "Systematic failure rate not found for subnet: {} and ts: {}", - metrics.subnet_assigned, metrics.ts - ) - .as_str(), - ); + .expect("Systematic failure rate not found"); let fr = if metrics.failure_rate < *systematic_fr { Decimal::ZERO } else { @@ -159,7 +153,7 @@ fn node_provider_rewards( nodes_active_fr.push(avg_fr); } } - let unassigned_fr: Decimal = if nodes_active_fr.len() > 0 { + let unassigned_fr: Decimal = if !nodes_active_fr.is_empty() { logger.execute( "Unassigned days failure rate:", Operation::Avg(nodes_active_fr), @@ -226,8 +220,7 @@ fn node_provider_rewards( LogEntry::IdiosyncraticFailureRates(daily_idiosyncratic_fr.clone()), ); - let multiplier_assigned = assigned_multiplier(logger, daily_idiosyncratic_fr); - multiplier_assigned + assigned_multiplier(logger, daily_idiosyncratic_fr) } else { logger.add_entry(LogLevel::Mid, LogEntry::NodeStatusUnassigned); @@ -289,10 +282,10 @@ fn systematic_fr_per_subnet( let mut subnet_daily_failure_rates: HashMap<(PrincipalId, u64), Vec> = HashMap::new(); - for (_, metrics) in daily_node_metrics { + for metrics in daily_node_metrics.values() { for metric in metrics { subnet_daily_failure_rates - .entry((metric.subnet_assigned.clone(), metric.ts)) + .entry((metric.subnet_assigned, metric.ts)) .or_default() .push(metric.failure_rate); } From bc2c3a7102e6c79be0004dfb10bf89f219f01f9b Mon Sep 17 00:00:00 2001 From: Pietro Date: Wed, 11 Dec 2024 16:10:04 +0100 Subject: [PATCH 16/20] Fix log displaying --- rs/registry/node_provider_rewards/src/v1_logs.rs | 13 +++++-------- rs/registry/node_provider_rewards/src/v1_rewards.rs | 1 - 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/rs/registry/node_provider_rewards/src/v1_logs.rs b/rs/registry/node_provider_rewards/src/v1_logs.rs index 6fda6aa8273..0a63bd5b36e 100644 --- a/rs/registry/node_provider_rewards/src/v1_logs.rs +++ b/rs/registry/node_provider_rewards/src/v1_logs.rs @@ -1,4 +1,3 @@ -use crate::v1_types::DailyNodeMetrics; use ic_base_types::PrincipalId; use itertools::Itertools; use rust_decimal::{prelude::Zero, Decimal}; @@ -106,7 +105,6 @@ pub enum LogEntry { }, ActiveIdiosyncraticFailureRates { node_id: PrincipalId, - daily_metrics: Vec, failure_rates: Vec, }, ComputeRewardsForNode { @@ -170,13 +168,12 @@ impl fmt::Display for LogEntry { } LogEntry::ActiveIdiosyncraticFailureRates { node_id, - daily_metrics, failure_rates, } => { write!( f, - "ActiveIdiosyncraticFailureRates | node_id={}, daily_metrics={:?}, failure_rates={}", - node_id, daily_metrics, failure_rates.len() + "ActiveIdiosyncraticFailureRates | node_id={}, failure_rates_discounted={:?}", + node_id, failure_rates ) } LogEntry::ComputeRewardsForNode { @@ -268,14 +265,14 @@ impl RewardsLog { result } - pub fn get_log(&self) -> String { + pub fn get_log(&self) -> Vec { self.entries .iter() .map(|(log_level, entry)| match log_level { - LogLevel::High => format!("\x1b[1m{}\x1b[0m", entry), + LogLevel::High => format!("{}", entry), LogLevel::Mid => format!(" - {}", entry), LogLevel::Low => format!(" - {}", entry), }) - .join("\n") + .collect_vec() } } diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index 38e3f9e539a..fbeee8b1642 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -103,7 +103,6 @@ fn nodes_idiosyncratic_fr( LogLevel::High, LogEntry::ActiveIdiosyncraticFailureRates { node_id: *node_id, - daily_metrics: daily_metrics.clone(), failure_rates: failure_rates.clone(), }, ); From 0ef1c3e7fbb5e3fdc7b280dc9e204b866f7b3dff Mon Sep 17 00:00:00 2001 From: Pietro Date: Wed, 11 Dec 2024 16:13:30 +0100 Subject: [PATCH 17/20] Additional whitespaces for logs --- rs/registry/node_provider_rewards/src/v1_logs.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/rs/registry/node_provider_rewards/src/v1_logs.rs b/rs/registry/node_provider_rewards/src/v1_logs.rs index 0a63bd5b36e..251c08acf93 100644 --- a/rs/registry/node_provider_rewards/src/v1_logs.rs +++ b/rs/registry/node_provider_rewards/src/v1_logs.rs @@ -270,8 +270,8 @@ impl RewardsLog { .iter() .map(|(log_level, entry)| match log_level { LogLevel::High => format!("{}", entry), - LogLevel::Mid => format!(" - {}", entry), - LogLevel::Low => format!(" - {}", entry), + LogLevel::Mid => format!(" - {}", entry), + LogLevel::Low => format!(" - {}", entry), }) .collect_vec() } From 318cabbc8006f729123ddcdbdd7e3d01e9f5f192 Mon Sep 17 00:00:00 2001 From: Pietro Date: Wed, 15 Jan 2025 09:56:23 +0100 Subject: [PATCH 18/20] Fixed comments --- .../node_provider_rewards/src/v1_logs.rs | 6 +- .../node_provider_rewards/src/v1_rewards.rs | 81 +++++++++---------- .../src/v1_rewards/tests.rs | 80 +++++++++--------- .../node_provider_rewards/src/v1_types.rs | 21 +++-- 4 files changed, 99 insertions(+), 89 deletions(-) diff --git a/rs/registry/node_provider_rewards/src/v1_logs.rs b/rs/registry/node_provider_rewards/src/v1_logs.rs index 251c08acf93..67c7ffeb287 100644 --- a/rs/registry/node_provider_rewards/src/v1_logs.rs +++ b/rs/registry/node_provider_rewards/src/v1_logs.rs @@ -1,4 +1,4 @@ -use ic_base_types::PrincipalId; +use ic_base_types::{NodeId, PrincipalId}; use itertools::Itertools; use rust_decimal::{prelude::Zero, Decimal}; use std::fmt; @@ -104,11 +104,11 @@ pub enum LogEntry { node_count: u32, }, ActiveIdiosyncraticFailureRates { - node_id: PrincipalId, + node_id: NodeId, failure_rates: Vec, }, ComputeRewardsForNode { - node_id: PrincipalId, + node_id: NodeId, node_type: String, region: String, }, diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index fbeee8b1642..ff7d279da1d 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -1,4 +1,4 @@ -use ic_base_types::PrincipalId; +use ic_base_types::{NodeId, PrincipalId, SubnetId}; use ic_protobuf::registry::node_rewards::v2::{NodeRewardRate, NodeRewardsTable}; use itertools::Itertools; use num_traits::ToPrimitive; @@ -25,38 +25,40 @@ const RF: &str = "Linear Reduction factor"; pub fn calculate_rewards( days_in_period: u64, rewards_table: &NodeRewardsTable, - subnet_metrics: HashMap>, + subnet_metrics: HashMap>, rewardable_nodes: &[RewardableNode], ) -> RewardsPerNodeProvider { let mut rewards_per_node_provider = HashMap::default(); let mut rewards_log_per_node_provider = HashMap::default(); - let mut all_assigned_metrics = daily_node_metrics(subnet_metrics); - let subnets_systematic_fr = systematic_fr_per_subnet(&all_assigned_metrics); - let node_provider_rewardables = rewardables_by_node_provider(rewardable_nodes); + let mut metrics_in_rewarding_period = metrics_in_rewarding_period(subnet_metrics); + let subnets_systematic_fr = systematic_fr_per_subnet(&metrics_in_rewarding_period); + let node_provider_rewardables = rewardable_nodes_by_node_provider(rewardable_nodes); - for (node_provider_id, node_provider_rewardables) in node_provider_rewardables { + for (node_provider_id, nodes) in node_provider_rewardables { let mut logger = RewardsLog::default(); logger.add_entry( LogLevel::High, LogEntry::CalculateRewardsForNodeProvider(node_provider_id), ); - let assigned_metrics: HashMap> = - node_provider_rewardables - .iter() - .filter_map(|node| { - all_assigned_metrics - .remove(&node.node_id) - .map(|daily_metrics| (node.node_id, daily_metrics)) - }) - .collect::>>(); - let node_daily_fr = - nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + let daily_metrics_by_node: HashMap> = nodes + .iter() + .filter_map(|node| { + metrics_in_rewarding_period + .remove(&node.node_id) + .map(|daily_metrics| (node.node_id, daily_metrics)) + }) + .collect(); + let node_daily_fr = compute_relative_node_failure_rate( + &mut logger, + &daily_metrics_by_node, + &subnets_systematic_fr, + ); let rewards = node_provider_rewards( &mut logger, - &node_provider_rewardables, + &nodes, node_daily_fr, days_in_period, rewards_table, @@ -72,18 +74,13 @@ pub fn calculate_rewards( } } -/// Computes the idiosyncratic daily failure rates for each node. -/// -/// This function calculates the idiosyncratic failure rates by subtracting the systematic -/// failure rate of the subnet from the node's failure rate for each day. -/// If the node's failure rate is less than the systematic failure rate, the idiosyncratic -/// failure rate is set to zero. -fn nodes_idiosyncratic_fr( +/// Computes the relative node failure rates discounting the subnet systematic failure rate. +fn compute_relative_node_failure_rate( logger: &mut RewardsLog, - assigned_metrics: &HashMap>, - subnets_systematic_fr: &HashMap<(PrincipalId, TimestampNanos), Decimal>, -) -> HashMap> { - let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); + assigned_metrics: &HashMap>, + subnets_systematic_fr: &HashMap<(SubnetId, TimestampNanos), Decimal>, +) -> HashMap> { + let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); for (node_id, daily_metrics) in assigned_metrics { let failure_rates = nodes_idiosyncratic_fr.entry(*node_id).or_default(); @@ -114,7 +111,7 @@ fn nodes_idiosyncratic_fr( fn node_provider_rewards( logger: &mut RewardsLog, rewardables: &[RewardableNode], - nodes_idiosyncratic_fr: HashMap>, + nodes_idiosyncratic_fr: HashMap>, days_in_period: u64, rewards_table: &NodeRewardsTable, ) -> Rewards { @@ -267,8 +264,8 @@ fn assigned_multiplier(logger: &mut RewardsLog, daily_failure_rate: Vec /// This function calculates the 75th percentile of failure rates for each subnet on a daily basis. /// This represents the systematic failure rate for all the nodes in the subnet for that day. fn systematic_fr_per_subnet( - daily_node_metrics: &HashMap>, -) -> HashMap<(PrincipalId, TimestampNanos), Decimal> { + daily_node_metrics: &HashMap>, +) -> HashMap<(SubnetId, TimestampNanos), Decimal> { fn percentile_75(mut values: Vec) -> Decimal { values.sort(); let len = values.len(); @@ -279,12 +276,12 @@ fn systematic_fr_per_subnet( values[idx] } - let mut subnet_daily_failure_rates: HashMap<(PrincipalId, u64), Vec> = HashMap::new(); + let mut subnet_daily_failure_rates: HashMap<(SubnetId, u64), Vec> = HashMap::new(); for metrics in daily_node_metrics.values() { for metric in metrics { subnet_daily_failure_rates - .entry((metric.subnet_assigned, metric.ts)) + .entry((metric.subnet_assigned.into(), metric.ts)) .or_default() .push(metric.failure_rate); } @@ -296,9 +293,9 @@ fn systematic_fr_per_subnet( .collect() } -fn daily_node_metrics( - subnets_metrics: HashMap>, -) -> HashMap> { +fn metrics_in_rewarding_period( + subnets_metrics: HashMap>, +) -> HashMap> { let mut subnets_metrics = subnets_metrics .into_iter() .flat_map(|(subnet_id, metrics)| { @@ -307,19 +304,19 @@ fn daily_node_metrics( .collect_vec(); subnets_metrics.sort_by_key(|(_, metrics)| metrics.timestamp_nanos); - let mut daily_node_metrics: HashMap> = + let mut metrics_in_rewarding_period: HashMap> = HashMap::default(); for (subnet_id, metrics) in subnets_metrics { for node_metrics in metrics.node_metrics { - daily_node_metrics - .entry(node_metrics.node_id) + metrics_in_rewarding_period + .entry(node_metrics.node_id.into()) .or_default() .push((subnet_id, metrics.timestamp_nanos, node_metrics)); } } - daily_node_metrics + metrics_in_rewarding_period .into_iter() .map(|(node_id, metrics)| { let mut daily_metrics = Vec::new(); @@ -514,7 +511,7 @@ fn base_rewards_region_nodetype( region_nodetype_rewards } -fn rewardables_by_node_provider( +fn rewardable_nodes_by_node_provider( nodes: &[RewardableNode], ) -> HashMap> { let mut node_provider_rewardables: HashMap> = diff --git a/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs index c8aec2de01c..359298111d5 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards/tests.rs @@ -21,7 +21,7 @@ impl MockedMetrics { } impl DailyNodeMetrics { - fn from_fr_dummy(ts: u64, subnet_assigned: PrincipalId, failure_rate: Decimal) -> Self { + fn from_fr_dummy(ts: u64, subnet_assigned: SubnetId, failure_rate: Decimal) -> Self { let num_blocks_proposed = 10; let num_blocks_failed = if failure_rate.is_zero() { 0 @@ -47,7 +47,7 @@ fn daily_mocked_failure_rates(metrics: Vec) -> Vec { (0..mocked_metrics.days).map(move |i| { DailyNodeMetrics::new( i, - PrincipalId::new_anonymous(), + PrincipalId::new_anonymous().into(), mocked_metrics.proposed_blocks, mocked_metrics.failed_blocks, ) @@ -84,8 +84,8 @@ fn mocked_rewards_table() -> NodeRewardsTable { } #[test] fn test_daily_node_metrics() { - let subnet1 = PrincipalId::new_user_test_id(1); - let subnet2 = PrincipalId::new_user_test_id(2); + let subnet1: SubnetId = PrincipalId::new_user_test_id(1).into(); + let subnet2: SubnetId = PrincipalId::new_user_test_id(2).into(); let node1 = PrincipalId::new_user_test_id(101); let node2 = PrincipalId::new_user_test_id(102); @@ -147,9 +147,9 @@ fn test_daily_node_metrics() { (subnet2, vec![sub2_day3]), ]); - let result = daily_node_metrics(input_metrics); + let result = metrics_in_rewarding_period(input_metrics); - let metrics_node1 = result.get(&node1).expect("Node1 metrics not found"); + let metrics_node1 = result.get(&node1.into()).expect("Node1 metrics not found"); assert_eq!(metrics_node1[0].subnet_assigned, subnet1); assert_eq!(metrics_node1[0].num_blocks_proposed, 10); assert_eq!(metrics_node1[0].num_blocks_failed, 2); @@ -162,7 +162,7 @@ fn test_daily_node_metrics() { assert_eq!(metrics_node1[2].num_blocks_proposed, 15); assert_eq!(metrics_node1[2].num_blocks_failed, 3); - let metrics_node2 = result.get(&node2).expect("Node2 metrics not found"); + let metrics_node2 = result.get(&node2.into()).expect("Node2 metrics not found"); assert_eq!(metrics_node2[0].subnet_assigned, subnet1); assert_eq!(metrics_node2[0].num_blocks_proposed, 20); assert_eq!(metrics_node2[0].num_blocks_failed, 5); @@ -269,14 +269,14 @@ fn test_same_rewards_percent_if_gaps_no_penalty() { } fn from_subnet_daily_metrics( - subnet_id: PrincipalId, + subnet_id: SubnetId, daily_subnet_fr: Vec<(TimestampNanos, Vec)>, -) -> HashMap> { +) -> HashMap> { let mut daily_node_metrics = HashMap::new(); for (day, fr) in daily_subnet_fr { fr.into_iter().enumerate().for_each(|(i, fr)| { let node_metrics: &mut Vec = daily_node_metrics - .entry(PrincipalId::new_user_test_id(i as u64)) + .entry(NodeId::from(PrincipalId::new_user_test_id(i as u64))) .or_default(); node_metrics.push(DailyNodeMetrics { @@ -291,7 +291,7 @@ fn from_subnet_daily_metrics( } #[test] fn test_systematic_fr_calculation() { - let subnet1 = PrincipalId::new_user_test_id(10); + let subnet1 = SubnetId::new(PrincipalId::new_user_test_id(1)); let assigned_metrics = from_subnet_daily_metrics( subnet1, @@ -306,7 +306,7 @@ fn test_systematic_fr_calculation() { let result = systematic_fr_per_subnet(&assigned_metrics); - let expected: HashMap<(PrincipalId, TimestampNanos), Decimal> = HashMap::from([ + let expected: HashMap<(SubnetId, TimestampNanos), Decimal> = HashMap::from([ ((subnet1, 1), dec!(0.3)), ((subnet1, 2), dec!(0.8)), ((subnet1, 3), dec!(0.64)), @@ -320,9 +320,9 @@ fn test_systematic_fr_calculation() { #[test] fn test_idiosyncratic_daily_fr_correct_values() { let mut logger = RewardsLog::default(); - let node1 = PrincipalId::new_user_test_id(1); - let node2 = PrincipalId::new_user_test_id(2); - let subnet1 = PrincipalId::new_user_test_id(10); + let node1 = NodeId::from(PrincipalId::new_user_test_id(1)); + let node2 = NodeId::from(PrincipalId::new_user_test_id(2)); + let subnet1 = SubnetId::from(PrincipalId::new_user_test_id(10)); let assigned_metrics = HashMap::from([ ( @@ -345,7 +345,8 @@ fn test_idiosyncratic_daily_fr_correct_values() { ((subnet1, 3), dec!(0.1)), ]); - let result = nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + let result = + compute_relative_node_failure_rate(&mut logger, &assigned_metrics, &subnets_systematic_fr); let expected = HashMap::from([ (node1, vec![dec!(0.1), dec!(0.3), dec!(0.749)]), // (0.2 - 0.1), (0.5 - 0.2), (0.849 - 0.1) @@ -359,8 +360,8 @@ fn test_idiosyncratic_daily_fr_correct_values() { #[should_panic(expected = "Systematic failure rate not found")] fn test_idiosyncratic_daily_fr_missing_systematic_fr() { let mut logger = RewardsLog::default(); - let node1 = PrincipalId::new_user_test_id(1); - let subnet1 = PrincipalId::new_user_test_id(10); + let node1: NodeId = PrincipalId::new_user_test_id(1).into(); + let subnet1: SubnetId = PrincipalId::new_user_test_id(10).into(); let assigned_metrics = HashMap::from([( node1, @@ -369,14 +370,14 @@ fn test_idiosyncratic_daily_fr_missing_systematic_fr() { let subnets_systematic_fr = HashMap::from([((subnet1, 2), dec!(0.1))]); - nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + compute_relative_node_failure_rate(&mut logger, &assigned_metrics, &subnets_systematic_fr); } #[test] fn test_idiosyncratic_daily_fr_negative_failure_rate() { let mut logger = RewardsLog::default(); - let node1 = PrincipalId::new_user_test_id(1); - let subnet1 = PrincipalId::new_user_test_id(10); + let node1: NodeId = PrincipalId::new_user_test_id(1).into(); + let subnet1: SubnetId = PrincipalId::new_user_test_id(10).into(); let assigned_metrics = HashMap::from([( node1, @@ -385,7 +386,8 @@ fn test_idiosyncratic_daily_fr_negative_failure_rate() { let subnets_systematic_fr = HashMap::from([((subnet1, 1), dec!(0.1))]); - let result = nodes_idiosyncratic_fr(&mut logger, &assigned_metrics, &subnets_systematic_fr); + let result = + compute_relative_node_failure_rate(&mut logger, &assigned_metrics, &subnets_systematic_fr); // Expecting zero due to saturation let expected = HashMap::from([(node1, vec![Decimal::ZERO])]); @@ -419,13 +421,13 @@ fn test_node_provider_below_min_limit() { let node_provider_id = PrincipalId::new_anonymous(); let rewardables = vec![ RewardableNode { - node_id: PrincipalId::new_user_test_id(1), + node_id: PrincipalId::new_user_test_id(1).into(), node_provider_id, region: "region1".to_string(), node_type: "type1".to_string(), }, RewardableNode { - node_id: PrincipalId::new_user_test_id(2), + node_id: PrincipalId::new_user_test_id(2).into(), node_provider_id, region: "region1".to_string(), node_type: "type3.1".to_string(), @@ -455,16 +457,16 @@ fn test_node_provider_rewards_one_assigned() { let rewardables = (1..=5) .map(|i| RewardableNode { - node_id: PrincipalId::new_user_test_id(i), + node_id: PrincipalId::new_user_test_id(i).into(), node_provider_id: PrincipalId::new_anonymous(), region: "A,B".to_string(), node_type: "type1".to_string(), }) .collect_vec(); - let mut nodes_idiosyncratic_fr = HashMap::new(); + let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); nodes_idiosyncratic_fr.insert( - PrincipalId::new_user_test_id(1), + PrincipalId::new_user_test_id(1).into(), vec![dec!(0.4), dec!(0.2), dec!(0.3), dec!(0.4)], // Avg. 0.325 ); @@ -523,20 +525,20 @@ fn test_node_provider_rewards_two_assigned() { let rewardables = (1..=5) .map(|i| RewardableNode { - node_id: PrincipalId::new_user_test_id(i), + node_id: PrincipalId::new_user_test_id(i).into(), node_provider_id: PrincipalId::new_anonymous(), region: "A,B".to_string(), node_type: "type1".to_string(), }) .collect_vec(); - let mut nodes_idiosyncratic_fr = HashMap::new(); + let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); nodes_idiosyncratic_fr.insert( - PrincipalId::new_user_test_id(1), + PrincipalId::new_user_test_id(1).into(), vec![dec!(0.4), dec!(0.2), dec!(0.3), dec!(0.4)], // Avg. 0.325 ); nodes_idiosyncratic_fr.insert( - PrincipalId::new_user_test_id(2), + PrincipalId::new_user_test_id(2).into(), vec![dec!(0.9), dec!(0.6), dec!(0.304), dec!(0.102)], // Avg. 0.4765 ); @@ -602,15 +604,15 @@ fn test_np_rewards_type3_coeff() { // 4 nodes in period: 1 assigned, 3 unassigned let rewardables = (1..=4) .map(|i| RewardableNode { - node_id: PrincipalId::new_user_test_id(i), + node_id: PrincipalId::new_user_test_id(i).into(), node_provider_id: PrincipalId::new_anonymous(), region: "A,B,C".to_string(), node_type: "type3.1".to_string(), }) .collect_vec(); - let mut nodes_idiosyncratic_fr = HashMap::new(); + let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); nodes_idiosyncratic_fr.insert( - PrincipalId::new_user_test_id(1), + PrincipalId::new_user_test_id(1).into(), vec![dec!(0.4), dec!(0.2), dec!(0.3), dec!(0.4)], // Avg. 0.325 ); @@ -671,7 +673,7 @@ fn test_np_rewards_type3_mix() { // 4 nodes in period: 1 assigned, 3 unassigned let mut rewardables = (1..=3) .map(|i| RewardableNode { - node_id: PrincipalId::new_user_test_id(i), + node_id: PrincipalId::new_user_test_id(i).into(), node_provider_id: PrincipalId::new_anonymous(), region: "A,B,C".to_string(), node_type: "type3.1".to_string(), @@ -679,19 +681,19 @@ fn test_np_rewards_type3_mix() { .collect_vec(); rewardables.push(RewardableNode { - node_id: PrincipalId::new_user_test_id(4), + node_id: PrincipalId::new_user_test_id(4).into(), node_provider_id: PrincipalId::new_anonymous(), region: "A,B,D".to_string(), node_type: "type3".to_string(), }); - let mut nodes_idiosyncratic_fr = HashMap::new(); + let mut nodes_idiosyncratic_fr: HashMap> = HashMap::new(); nodes_idiosyncratic_fr.insert( - PrincipalId::new_user_test_id(3), + PrincipalId::new_user_test_id(3).into(), vec![dec!(0.1), dec!(0.12), dec!(0.23), dec!(0.12)], ); nodes_idiosyncratic_fr.insert( - PrincipalId::new_user_test_id(4), + PrincipalId::new_user_test_id(4).into(), vec![dec!(0.2), dec!(0.32), dec!(0.123), dec!(0.432)], ); diff --git a/rs/registry/node_provider_rewards/src/v1_types.rs b/rs/registry/node_provider_rewards/src/v1_types.rs index 9490aa30068..52a51cee17d 100644 --- a/rs/registry/node_provider_rewards/src/v1_types.rs +++ b/rs/registry/node_provider_rewards/src/v1_types.rs @@ -1,4 +1,4 @@ -use ic_base_types::PrincipalId; +use ic_base_types::{NodeId, PrincipalId, SubnetId}; use ic_management_canister_types::NodeMetricsHistoryResponse; use num_traits::FromPrimitive; use rust_decimal::Decimal; @@ -13,21 +13,32 @@ pub type SubnetMetricsHistory = (PrincipalId, Vec); #[derive(Clone, Hash, Eq, PartialEq)] pub struct RewardableNode { - pub node_id: PrincipalId, + pub node_id: NodeId, pub node_provider_id: PrincipalId, pub region: String, pub node_type: String, } -#[derive(Clone, Hash, Eq, PartialEq, Debug, Default)] +#[derive(Clone, Hash, Eq, PartialEq, Debug)] pub struct DailyNodeMetrics { pub ts: u64, - pub subnet_assigned: PrincipalId, + pub subnet_assigned: SubnetId, pub num_blocks_proposed: u64, pub num_blocks_failed: u64, pub failure_rate: Decimal, } +impl Default for DailyNodeMetrics { + fn default() -> Self { + DailyNodeMetrics { + ts: 0, + subnet_assigned: SubnetId::from(PrincipalId::new_anonymous()), + num_blocks_proposed: 0, + num_blocks_failed: 0, + failure_rate: Decimal::ZERO, + } + } +} impl fmt::Display for DailyNodeMetrics { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!( @@ -41,7 +52,7 @@ impl fmt::Display for DailyNodeMetrics { impl DailyNodeMetrics { pub fn new( ts: u64, - subnet_assigned: PrincipalId, + subnet_assigned: SubnetId, num_blocks_proposed: u64, num_blocks_failed: u64, ) -> Self { From 17625dfae3d5b978a17e50c9a776b061dbff3cb9 Mon Sep 17 00:00:00 2001 From: IDX GitHub Automation Date: Wed, 15 Jan 2025 09:07:38 +0000 Subject: [PATCH 19/20] Automatically updated Cargo*.lock --- Cargo.Bazel.Fuzzing.json.lock | 159 ++++++++++++++++++++++++++++++---- Cargo.Bazel.json.lock | 159 ++++++++++++++++++++++++++++++---- 2 files changed, 288 insertions(+), 30 deletions(-) diff --git a/Cargo.Bazel.Fuzzing.json.lock b/Cargo.Bazel.Fuzzing.json.lock index 08fd98a11dc..c4032abf4a2 100644 --- a/Cargo.Bazel.Fuzzing.json.lock +++ b/Cargo.Bazel.Fuzzing.json.lock @@ -1,5 +1,5 @@ { - "checksum": "befed3db2258ef5e97774c44951feb5c8ca098af84913123d4a8945afeac827c", + "checksum": "35ef034b295f0b24c8651990aa834145ac9dd68a479ab6ebca823a9cd5256a0d", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -1465,6 +1465,8 @@ ], "crate_features": { "common": [ + "compile-time-rng", + "const-random", "default", "getrandom", "runtime-rng", @@ -1482,6 +1484,10 @@ "id": "cfg-if 1.0.0", "target": "cfg_if" }, + { + "id": "const-random 0.1.18", + "target": "const_random" + }, { "id": "getrandom 0.2.10", "target": "getrandom" @@ -13827,6 +13833,110 @@ ], "license_file": "LICENSE-APACHE" }, + "const-random 0.1.18": { + "name": "const-random", + "version": "0.1.18", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random/0.1.18/download", + "sha256": "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" + } + }, + "targets": [ + { + "Library": { + "crate_name": "const_random", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "const-random-macro 0.1.16", + "target": "const_random_macro" + } + ], + "selects": {} + }, + "version": "0.1.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "const-random-macro 0.1.16": { + "name": "const-random-macro", + "version": "0.1.16", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random-macro/0.1.16/download", + "sha256": "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "const_random_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "getrandom 0.2.10", + "target": "getrandom" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "tiny-keccak 2.0.2", + "target": "tiny_keccak" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.16" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "convert_case 0.4.0": { "name": "convert_case", "version": "0.4.0", @@ -18351,6 +18461,10 @@ "id": "addr 0.15.6", "target": "addr" }, + { + "id": "ahash 0.8.11", + "target": "ahash" + }, { "id": "aide 0.13.4", "target": "aide" @@ -72259,46 +72373,60 @@ ], "selects": { "aarch64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "aarch64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ], "arm-unknown-linux-gnueabi": [ - "sha3" + "sha3", + "shake" ], "i686-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "i686-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "powerpc-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "s390x-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "x86_64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-freebsd": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ] } }, @@ -87303,6 +87431,7 @@ "actix-rt 2.10.0", "actix-web 4.9.0", "addr 0.15.6", + "ahash 0.8.11", "aide 0.13.4", "anyhow 1.0.93", "arbitrary 1.3.2", diff --git a/Cargo.Bazel.json.lock b/Cargo.Bazel.json.lock index b5e8dae02b1..28157296b55 100644 --- a/Cargo.Bazel.json.lock +++ b/Cargo.Bazel.json.lock @@ -1,5 +1,5 @@ { - "checksum": "16c350a57ca08666035e4f0e31f17c9715860d84ed6755b026870a84de503a09", + "checksum": "fe8b0cdc955eee4e9086b15b5cfbbce1f3c847dfb81b961cb895dd6b1d026db8", "crates": { "abnf 0.12.0": { "name": "abnf", @@ -1469,6 +1469,8 @@ ], "crate_features": { "common": [ + "compile-time-rng", + "const-random", "default", "getrandom", "runtime-rng", @@ -1486,6 +1488,10 @@ "id": "cfg-if 1.0.0", "target": "cfg_if" }, + { + "id": "const-random 0.1.18", + "target": "const_random" + }, { "id": "getrandom 0.2.10", "target": "getrandom" @@ -13655,6 +13661,110 @@ ], "license_file": "LICENSE-APACHE" }, + "const-random 0.1.18": { + "name": "const-random", + "version": "0.1.18", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random/0.1.18/download", + "sha256": "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" + } + }, + "targets": [ + { + "Library": { + "crate_name": "const_random", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "edition": "2018", + "proc_macro_deps": { + "common": [ + { + "id": "const-random-macro 0.1.16", + "target": "const_random_macro" + } + ], + "selects": {} + }, + "version": "0.1.18" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, + "const-random-macro 0.1.16": { + "name": "const-random-macro", + "version": "0.1.16", + "package_url": "https://github.com/tkaitchuck/constrandom", + "repository": { + "Http": { + "url": "https://static.crates.io/crates/const-random-macro/0.1.16/download", + "sha256": "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" + } + }, + "targets": [ + { + "ProcMacro": { + "crate_name": "const_random_macro", + "crate_root": "src/lib.rs", + "srcs": { + "allow_empty": true, + "include": [ + "**/*.rs" + ] + } + } + } + ], + "library_target_name": "const_random_macro", + "common_attrs": { + "compile_data_glob": [ + "**" + ], + "deps": { + "common": [ + { + "id": "getrandom 0.2.10", + "target": "getrandom" + }, + { + "id": "once_cell 1.19.0", + "target": "once_cell" + }, + { + "id": "tiny-keccak 2.0.2", + "target": "tiny_keccak" + } + ], + "selects": {} + }, + "edition": "2018", + "version": "0.1.16" + }, + "license": "MIT OR Apache-2.0", + "license_ids": [ + "Apache-2.0", + "MIT" + ], + "license_file": "LICENSE-APACHE" + }, "convert_case 0.4.0": { "name": "convert_case", "version": "0.4.0", @@ -18179,6 +18289,10 @@ "id": "addr 0.15.6", "target": "addr" }, + { + "id": "ahash 0.8.11", + "target": "ahash" + }, { "id": "aide 0.13.4", "target": "aide" @@ -72105,46 +72219,60 @@ ], "selects": { "aarch64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "aarch64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "aarch64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ], "arm-unknown-linux-gnueabi": [ - "sha3" + "sha3", + "shake" ], "i686-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "i686-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "powerpc-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "s390x-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-apple-darwin": [ - "sha3" + "sha3", + "shake" ], "x86_64-pc-windows-msvc": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-freebsd": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-linux-gnu": [ - "sha3" + "sha3", + "shake" ], "x86_64-unknown-nixos-gnu": [ - "sha3" + "sha3", + "shake" ] } }, @@ -87182,6 +87310,7 @@ "actix-rt 2.10.0", "actix-web 4.9.0", "addr 0.15.6", + "ahash 0.8.11", "aide 0.13.4", "anyhow 1.0.93", "arbitrary 1.3.2", From d62d8d8ebb3d2ccdd7a27389de8a4f1f1436fc2e Mon Sep 17 00:00:00 2001 From: Pietro Date: Wed, 15 Jan 2025 10:23:01 +0100 Subject: [PATCH 20/20] Remove useless into() --- rs/registry/node_provider_rewards/src/v1_rewards.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/registry/node_provider_rewards/src/v1_rewards.rs b/rs/registry/node_provider_rewards/src/v1_rewards.rs index ff7d279da1d..df12de45003 100644 --- a/rs/registry/node_provider_rewards/src/v1_rewards.rs +++ b/rs/registry/node_provider_rewards/src/v1_rewards.rs @@ -281,7 +281,7 @@ fn systematic_fr_per_subnet( for metrics in daily_node_metrics.values() { for metric in metrics { subnet_daily_failure_rates - .entry((metric.subnet_assigned.into(), metric.ts)) + .entry((metric.subnet_assigned, metric.ts)) .or_default() .push(metric.failure_rate); }