diff --git a/Cargo.Bazel.lock b/Cargo.Bazel.lock index f770c6841..8ee99fb3b 100644 --- a/Cargo.Bazel.lock +++ b/Cargo.Bazel.lock @@ -1,5 +1,5 @@ { - "checksum": "8f8316053a9bbdfb2e006bb7300906283da5cbb9888e2eaaab85777bd8924e11", + "checksum": "2bbd3d15bf606c9af001e99012b5deb363a91b712bf83a21702c95fdf0ccdf21", "crates": { "actix-codec 0.5.2": { "name": "actix-codec", @@ -21383,7 +21383,7 @@ "target": "prost" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" }, { @@ -21658,7 +21658,7 @@ "target": "prost" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" }, { @@ -21961,7 +21961,7 @@ "target": "lazy_static" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" }, { @@ -21979,7 +21979,7 @@ "proc_macro_deps": { "common": [ { - "id": "rust_decimal_macros 1.35.0", + "id": "rust_decimal_macros 1.36.0", "target": "rust_decimal_macros" } ], @@ -22397,7 +22397,7 @@ "target": "registry_canister" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" }, { @@ -22442,7 +22442,7 @@ "target": "ic_nervous_system_common_build_metadata" }, { - "id": "rust_decimal_macros 1.35.0", + "id": "rust_decimal_macros 1.36.0", "target": "rust_decimal_macros" }, { @@ -24146,7 +24146,7 @@ "target": "rand_chacha" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" }, { @@ -24176,7 +24176,7 @@ "target": "ic_nervous_system_common_build_metadata" }, { - "id": "rust_decimal_macros 1.35.0", + "id": "rust_decimal_macros 1.36.0", "target": "rust_decimal_macros" }, { @@ -24293,7 +24293,7 @@ "target": "num_traits" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" } ], @@ -24303,7 +24303,7 @@ "proc_macro_deps": { "common": [ { - "id": "rust_decimal_macros 1.35.0", + "id": "rust_decimal_macros 1.36.0", "target": "rust_decimal_macros" } ], @@ -24392,7 +24392,7 @@ "target": "mockall" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" } ], @@ -24832,7 +24832,7 @@ "target": "prost" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" }, { @@ -24854,7 +24854,7 @@ "target": "async_trait" }, { - "id": "rust_decimal_macros 1.35.0", + "id": "rust_decimal_macros 1.36.0", "target": "rust_decimal_macros" } ], @@ -37794,13 +37794,13 @@ }, "license": "MIT OR Apache-2.0" }, - "rust_decimal 1.35.0": { + "rust_decimal 1.36.0": { "name": "rust_decimal", - "version": "1.35.0", + "version": "1.36.0", "repository": { "Http": { - "url": "https://static.crates.io/crates/rust_decimal/1.35.0/download", - "sha256": "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" + "url": "https://static.crates.io/crates/rust_decimal/1.36.0/download", + "sha256": "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" } }, "targets": [ @@ -37847,7 +37847,7 @@ "target": "num_traits" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "build_script_build" }, { @@ -37858,7 +37858,7 @@ "selects": {} }, "edition": "2021", - "version": "1.35.0" + "version": "1.36.0" }, "build_script_attrs": { "data_glob": [ @@ -37867,13 +37867,13 @@ }, "license": "MIT" }, - "rust_decimal_macros 1.35.0": { + "rust_decimal_macros 1.36.0": { "name": "rust_decimal_macros", - "version": "1.35.0", + "version": "1.36.0", "repository": { "Http": { - "url": "https://static.crates.io/crates/rust_decimal_macros/1.35.0/download", - "sha256": "a05bf7103af0797dbce0667c471946b29b9eaea34652eff67324f360fec027de" + "url": "https://static.crates.io/crates/rust_decimal_macros/1.36.0/download", + "sha256": "da991f231869f34268415a49724c6578e740ad697ba0999199d6f22b3949332c" } }, "targets": [ @@ -37905,14 +37905,14 @@ "target": "quote" }, { - "id": "rust_decimal 1.35.0", + "id": "rust_decimal 1.36.0", "target": "rust_decimal" } ], "selects": {} }, "edition": "2021", - "version": "1.35.0" + "version": "1.36.0" }, "license": "MIT" }, @@ -45528,6 +45528,14 @@ { "id": "itertools 0.13.0", "target": "itertools" + }, + { + "id": "num-traits 0.2.19", + "target": "num_traits" + }, + { + "id": "rust_decimal 1.36.0", + "target": "rust_decimal" } ], "selects": {} @@ -45538,6 +45546,10 @@ { "id": "ic-cdk-macros 0.15.0", "target": "ic_cdk_macros" + }, + { + "id": "rust_decimal_macros 1.36.0", + "target": "rust_decimal_macros" } ], "selects": {} diff --git a/Cargo.lock b/Cargo.lock index 0d6658a16..e8099275a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7591,9 +7591,9 @@ dependencies = [ [[package]] name = "rust_decimal" -version = "1.35.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1790d1c4c0ca81211399e0e0af16333276f375209e71a37b67698a373db5b47a" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" dependencies = [ "arrayvec 0.7.4", "borsh", @@ -7607,9 +7607,9 @@ dependencies = [ [[package]] name = "rust_decimal_macros" -version = "1.35.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a05bf7103af0797dbce0667c471946b29b9eaea34652eff67324f360fec027de" +checksum = "da991f231869f34268415a49724c6578e740ad697ba0999199d6f22b3949332c" dependencies = [ "quote", "rust_decimal", @@ -9077,6 +9077,9 @@ dependencies = [ "ic-registry-keys", "ic-stable-structures", "itertools 0.13.0", + "num-traits", + "rust_decimal", + "rust_decimal_macros", "trustworthy-node-metrics-types", ] diff --git a/Cargo.toml b/Cargo.toml index c5c136680..e36153366 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -152,6 +152,7 @@ keyring = "3.2.0" lazy_static = "1.5.0" log = "0.4.22" lru = "0.12.4" +num-traits = "0.2" opentelemetry = { version = "0.22.0", features = ["metrics"] } phantom_newtype = { git = "https://github.com/dfinity/ic.git", rev = "26d5f9d0bdca0a817c236134dc9c7317b32c69a5" } pkcs11 = "0.5.0" @@ -173,6 +174,8 @@ retry = "2.0.0" reverse_geocoder = "4.1.1" ring = "0.17.8" rstest = { version = "0.22.0", default-features = false } +rust_decimal = "1.36.0" # or the latest version +rust_decimal_macros = "1.36.0" # optional, for using macros serde = { version = "1.0", features = ["derive"] } serde_derive = "1.0.209" serde_json = "1.0.127" diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx index cbd1f6a53..26114e7d4 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/App.tsx @@ -68,7 +68,7 @@ const App: React.FC = () => { to_ts: dateToNanoseconds(periodFilter.dateEnd), }; const nodeRewardsResponse = await trustworthy_node_metrics.node_rewards(request); - const sortedNodeRewards = nodeRewardsResponse.sort((a, b) => a.rewards_percent - b.rewards_percent); + const sortedNodeRewards = nodeRewardsResponse.sort((a, b) => a.rewards_computation.rewards_percent - b.rewards_computation.rewards_percent); const subnets = new Set(sortedNodeRewards.flatMap(node => node.daily_node_metrics.map(data => data.subnet_assigned.toText()))); const providers = new Set(sortedNodeRewards.flatMap(node => node.node_provider_id.toText())); diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeList.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeList.tsx index 6caa20b69..744a0abe7 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeList.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeList.tsx @@ -79,8 +79,8 @@ export const NodeList: React.FC = ({ nodeRewards }) => { {nodeMetrics.node_provider_id.toText()} {nodeMetrics.daily_node_metrics.length} - {Math.round(nodeMetrics.rewards_percent * 100)}% - {Math.round(nodeMetrics.rewards_stats.failure_rate * 100)}% + {Math.round(nodeMetrics.rewards_computation.rewards_percent * 100)}% + {Math.round(nodeMetrics.rewards_computation.failure_rate * 100)}% ))} diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx index 30a7cabf9..c4d082646 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodePage.tsx @@ -17,7 +17,7 @@ export interface NodePageProps { periodFilter: PeriodFilter; } -const NodeMetricsStats: React.FC<{ stats: NodeRewardsResponse['rewards_stats'] }> = ({ stats }) => ( +const NodeMetricsStats: React.FC<{ stats: NodeRewardsResponse['rewards_computation'] }> = ({ stats }) => ( @@ -40,8 +40,8 @@ export const NodePage: React.FC = ({ nodeRewards, periodFilter }) } const chartDailyData: ChartData[] = generateChartData(periodFilter, nodeMetrics.daily_node_metrics); - const failureRateAvg = Math.round(nodeMetrics.rewards_stats.failure_rate * 100); - const rewardsPercent = Math.round(nodeMetrics.rewards_percent * 100); + const failureRateAvg = Math.round(nodeMetrics.rewards_computation.failure_rate * 100); + const rewardsPercent = Math.round(nodeMetrics.rewards_computation.rewards_percent * 100); const rewardsReduction = 100 - rewardsPercent; const rows: GridRowsProp = nodeMetrics.daily_node_metrics.map((data, index) => { @@ -81,7 +81,7 @@ export const NodePage: React.FC = ({ nodeRewards, periodFilter }) - + diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeProviderPage.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeProviderPage.tsx index dc1b7e84a..d66a73da8 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeProviderPage.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/NodeProviderPage.tsx @@ -22,7 +22,7 @@ export const NodeProviderPage: React.FC = ({ nodeRewards, const providerNodeMetrics = nodeRewards .filter((nodeMetrics) => nodeMetrics.node_provider_id.toText() === provider) const highFailureRateChart = providerNodeMetrics - .filter(nodeMetrics => nodeMetrics.rewards_stats.rewards_reduction > 0) + .filter(nodeMetrics => nodeMetrics.rewards_computation.rewards_reduction > 0) .flatMap(nodeMetrics => { const chartData = generateChartData(periodFilter, nodeMetrics.daily_node_metrics); return { diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardTable.tsx b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardTable.tsx index f54d43f14..b404cbca7 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardTable.tsx +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-frontend/src/components/RewardTable.tsx @@ -35,7 +35,7 @@ const RewardTable: React.FC = ({ nodeRewards }) => { {nodeMetrics.node_id.toText()} - {Math.round(nodeMetrics.rewards_stats.failure_rate * 100)}% + {nodeMetrics.rewards_computation.failure_rate * 100}% ))} diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-types/src/types.rs b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-types/src/types.rs index c5a545a64..2698d34fd 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-types/src/types.rs +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics-types/src/types.rs @@ -1,4 +1,4 @@ -use std::borrow::Cow; +use std::{borrow::Cow, fmt}; use candid::{CandidType, Decode, Deserialize, Encode, Principal}; use dfn_core::api::PrincipalId; @@ -79,6 +79,16 @@ pub struct DailyNodeMetrics { pub failure_rate: f64, } +impl fmt::Display for DailyNodeMetrics { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "timestamp_nanoseconds: {}, num_blocks_proposed: {}, num_blocks_failed: {}", + self.ts, self.num_blocks_proposed, self.num_blocks_failed + ) + } +} + impl DailyNodeMetrics { pub fn new(ts: TimestampNanos, subnet_assignment: Principal, proposed_blocks: u64, failed_blocks: u64) -> Self { let total_blocks = failed_blocks + proposed_blocks; @@ -99,19 +109,20 @@ impl DailyNodeMetrics { } #[derive(Debug, Deserialize, CandidType)] -pub struct RewardsStats { +pub struct RewardsComputationResult { + pub rewards_percent: f64, + pub rewards_reduction: f64, pub blocks_failed: u64, pub blocks_proposed: u64, pub blocks_total: u64, pub failure_rate: f64, - pub rewards_reduction: f64, + pub computation_log: String, } #[derive(Debug, Deserialize, CandidType)] pub struct NodeRewardsResponse { pub node_id: Principal, pub node_provider_id: Principal, - pub rewards_percent: f64, pub daily_node_metrics: Vec, - pub rewards_stats: RewardsStats, + pub rewards_computation: RewardsComputationResult, } diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/Cargo.toml b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/Cargo.toml index 5514589e3..6b2b59d11 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/Cargo.toml +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/Cargo.toml @@ -27,3 +27,6 @@ dfn_core = { workspace = true } trustworthy-node-metrics-types = { workspace = true } ic-base-types = { workspace = true } ic-registry-keys = { workspace = true } +rust_decimal = { workspace = true } +rust_decimal_macros = { workspace = true } +num-traits = { workspace = true } diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/computation_logger.rs b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/computation_logger.rs new file mode 100644 index 000000000..2b2d98837 --- /dev/null +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/computation_logger.rs @@ -0,0 +1,128 @@ +use std::fmt; + +use itertools::Itertools; +use num_traits::Zero; +use rust_decimal::Decimal; + +pub enum Operation { + Set(Decimal), + Sum(Vec), + Subtract(Decimal, Decimal), + Divide(Decimal, Decimal), +} + +impl Operation { + fn execute(&self) -> Decimal { + match self { + Operation::Sum(operators) => operators.iter().cloned().fold(Decimal::zero(), |acc, val| acc + val), + Operation::Subtract(o1, o2) => o1 - o2, + Operation::Divide(o1, o2) => o1 / o2, + Operation::Set(o1) => *o1, + } + } +} + +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, + "{} + {}", + values[0], + values[1..].iter().map(|o| format!("{}", o.round_dp(4))).collect::>().join(" + ") + ) + } + Operation::Subtract(o1, o2) => ("-", o1, o2), + Operation::Divide(o1, o2) => ("/", o1, o2), + Operation::Set(o1) => return write!(f, "{}", o1), + }; + write!(f, "{} {} {}", o1.round_dp(4), symbol, o2.round_dp(4)) + } +} + +pub struct OperationExecutor { + reason: String, + operation: Operation, + result: Decimal, +} + +impl OperationExecutor { + pub fn execute(reason: &str, operation: Operation) -> (Self, Decimal) { + let result = operation.execute(); + + let operation_executed = Self { + reason: reason.to_string(), + operation, + result, + }; + + (operation_executed, result) + } +} + +impl fmt::Display for OperationExecutor { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self.operation { + Operation::Set(x) => writeln!(f, "{}: {}", self.reason, x)?, + _ => writeln!(f, "{}: {} = {}", self.reason, self.operation, self.result.round_dp(4))?, + } + Ok(()) + } +} + +// Modify ComputationLogger to use NumberEnum +pub struct ComputationLogger { + maybe_input: Option, + operations_executed: Vec, +} + +impl ComputationLogger { + pub fn new() -> Self { + Self { + maybe_input: None, + operations_executed: Vec::new(), + } + } + + pub fn with_input(self, input: String) -> Self { + Self { + maybe_input: Some(input), + ..self + } + } + + pub fn execute(&mut self, reason: &str, operation: Operation) -> Decimal { + let result = operation.execute(); + + let operation_executed = OperationExecutor { + reason: reason.to_string(), + operation, + result, + }; + self.operations_executed.push(operation_executed); + result + } + + pub fn add_executed(&mut self, operations: Vec) { + for operation in operations { + self.operations_executed.push(operation) + } + } + + pub fn get_log(&self) -> String { + let operations_log = self + .operations_executed + .iter() + .enumerate() + .map(|(index, item)| format!("STEP {}: {}", index + 1, item)) + .collect_vec() + .join("\n"); + + if let Some(input) = &self.maybe_input { + format!("INPUT:\n{}\nCOMPUTATION LOG:\n\n{}", input, operations_log) + } else { + format!("COMPUTATION LOG:\n\n{}", operations_log) + } + } +} diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/lib.rs b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/lib.rs index c908ec871..aafdbcf45 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/lib.rs +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/lib.rs @@ -6,6 +6,7 @@ use trustworthy_node_metrics_types::types::{ DailyNodeMetrics, NodeMetrics, NodeMetricsStored, NodeMetricsStoredKey, NodeRewardsArgs, NodeRewardsResponse, SubnetNodeMetricsArgs, SubnetNodeMetricsResponse, }; +mod computation_logger; mod metrics_manager; mod rewards_manager; mod stable_memory; @@ -111,15 +112,14 @@ fn node_rewards(args: NodeRewardsArgs) -> Vec { daily_metrics .into_iter() .map(|(node_id, daily_node_metrics)| { - let (rewards_percent, rewards_stats) = rewards_manager::compute_rewards_percent(&daily_node_metrics); + let rewards_computation = rewards_manager::compute_rewards_percent(&daily_node_metrics); let node_provider_id = stable_memory::get_node_provider(&node_id).unwrap_or(Principal::anonymous()); NodeRewardsResponse { node_id, node_provider_id, - rewards_percent, daily_node_metrics, - rewards_stats, + rewards_computation, } }) .collect_vec() diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/rewards_manager.rs b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/rewards_manager.rs index 47c357690..15f8e77fc 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/rewards_manager.rs +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/src/rewards_manager.rs @@ -1,20 +1,26 @@ -use trustworthy_node_metrics_types::types::{DailyNodeMetrics, RewardsStats}; +use itertools::Itertools; +use num_traits::ToPrimitive; +use rust_decimal::Decimal; +use rust_decimal_macros::dec; +use trustworthy_node_metrics_types::types::{DailyNodeMetrics, RewardsComputationResult}; -const MIN_FAILURE_RATE: f64 = 0.1; -const MAX_FAILURE_RATE: f64 = 0.7; +use crate::computation_logger::{ComputationLogger, Operation, OperationExecutor}; + +const MIN_FAILURE_RATE: Decimal = dec!(0.1); +const MAX_FAILURE_RATE: Decimal = dec!(0.7); /// Calculates the rewards reduction based on the failure rate. /// /// # Arguments /// -/// * `failure_rate` - A reference to a `f64` value representing the failure rate. +/// * `failure_rate` - A reference to a `Decimal` value representing the failure rate. /// /// # Returns /// -/// * A `f64` value representing the rewards reduction, where: -/// - `0.0` indicates no reduction (failure rate below the minimum threshold), -/// - `1.0` indicates maximum reduction (failure rate above the maximum threshold), -/// - A value between `0.0` and `1.0` represents a proportional reduction based on the failure rate. +/// * A `Decimal` value representing the rewards reduction, where: +/// - `0` indicates no reduction (failure rate below the minimum threshold), +/// - `1` indicates maximum reduction (failure rate above the maximum threshold), +/// - A value between `0` and `1` represents a proportional reduction based on the failure rate. /// /// # Explanation /// @@ -23,73 +29,42 @@ const MAX_FAILURE_RATE: f64 = 0.7; /// 2. It then checks if the `failure_rate` is above the `MAX_FAILURE_RATE` -> maximum reduction in rewards. /// /// 3. If the `failure_rate` is within the defined range (`MIN_FAILURE_RATE` to `MAX_FAILURE_RATE`), -/// the function calculates the reduction proportionally: -/// - The reduction is calculated by normalizing the `failure_rate` within the range, resulting in a value between `0.0` and `1.0`. -fn rewards_reduction(failure_rate: &f64) -> f64 { +/// the function calculates the reduction proportionally. +fn rewards_reduction_percent(failure_rate: &Decimal) -> (Vec, Decimal) { + const RF: &str = "Linear Reduction factor"; + if failure_rate < &MIN_FAILURE_RATE { - 0.0 + let (operation, result) = OperationExecutor::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)), + ); + (vec![operation], result) } else if failure_rate > &MAX_FAILURE_RATE { - 1.0 - } else { - (failure_rate - MIN_FAILURE_RATE) / (MAX_FAILURE_RATE - MIN_FAILURE_RATE) - } -} + let (operation, result) = OperationExecutor::execute( + &format!( + "Max reduction applied because {} is over {} failure rate.\n{}", + failure_rate.round_dp(4), + MAX_FAILURE_RATE, + RF + ), + Operation::Set(dec!(1)), + ); -/// Computes the rewards percentage based on the daily reward reductions WITH consecutive days penalty. -/// -/// # Arguments -/// -/// * `daily_metrics` - A slice of `DailyNodeMetrics` structs, where each struct represents the metrics for a single day. -/// -/// # Returns -/// -/// * A `f64` value representing the rewards percentage after reductions and penalties, rounded to two decimal places. -/// -/// # Explanation -/// -/// 1. The function calculates the number of active days. -/// -/// 2. The function iterates over each day's metrics: -/// - For each day with a non-zero reduction, it adds the reduction to the cumulative penalty and increments the consecutive day penalty counter. -/// - For each day with zero reduction (`daily_reduction == 0.0`), if there was a streak of consecutive days with reductions, -/// it adds the cumulative penalty to the `reduction_sum` and resets the streak counters. -/// -/// 3. The overall reduction is calculated by dividing the total reduction (`reduction_sum`) by the number of active days. -/// The reduction is then normalized by ensuring it does not exceed 1.0. -#[allow(dead_code)] -pub fn rewards_with_penalty(daily_metrics: &[DailyNodeMetrics]) -> f64 { - let active_days = daily_metrics.len(); - let mut reduction_sum = 0.0; - let mut consecutive_reduction = 0.0; - let mut consecutive_count = 0; - - for metrics in daily_metrics.iter() { - // Just if we want to count the day unassigned as 0.0 reduction - // we would need to check if previous daily metrics is <= 24hrs - // before current metrics - let daily_reduction: f64 = metrics.num_blocks_failed as f64 / (metrics.num_blocks_failed + metrics.num_blocks_proposed) as f64; - - if daily_reduction == 0.0 { - if consecutive_count > 0 { - reduction_sum += consecutive_reduction * consecutive_count as f64; - consecutive_reduction = 0.0; - consecutive_count = 0; - } - } else { - consecutive_reduction += daily_reduction; - consecutive_count += 1; - } - } + (vec![operation], result) + } else { + let (y_change_operation, y_change) = + OperationExecutor::execute("Linear Reduction Y change", Operation::Subtract(*failure_rate, MIN_FAILURE_RATE)); + let (x_change_operation, x_change) = + OperationExecutor::execute("Linear Reduction X change", Operation::Subtract(MAX_FAILURE_RATE, MIN_FAILURE_RATE)); - // Handles the last consecutive days - if consecutive_count > 0 { - reduction_sum += consecutive_reduction * consecutive_count as f64; + let (operation, result) = OperationExecutor::execute(RF, Operation::Divide(y_change, x_change)); + (vec![y_change_operation, x_change_operation, operation], result) } - - let overall_reduction = reduction_sum / active_days as f64; - let reduction_normalized = overall_reduction.min(1.0); - - ((1.0 - reduction_normalized) * 100.0).round() / 100.0 } /// Compute rewards percent @@ -102,7 +77,7 @@ pub fn rewards_with_penalty(daily_metrics: &[DailyNodeMetrics]) -> f64 { /// /// # Returns /// -/// * A `f64` value representing the rewards percentage left after the rewards reduction, rounded to two decimal places. +/// * A `RewardsComputationResult`. /// /// # Explanation /// @@ -110,33 +85,37 @@ pub fn rewards_with_penalty(daily_metrics: &[DailyNodeMetrics]) -> f64 { /// 2. The `overall_failure_rate` 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 percentage to be distrubuted to the node is computed. -pub fn compute_rewards_percent(daily_metrics: &[DailyNodeMetrics]) -> (f64, RewardsStats) { - let (overall_failed, overall_proposed): (u64, u64) = daily_metrics - .iter() - .map(|metrics| { - let daily_failed = metrics.num_blocks_failed; - let daily_proposed = metrics.num_blocks_proposed; - - (daily_failed, daily_proposed) - }) - .fold((0, 0), |(failed_acc, total_acc), (failed, total)| { - (failed_acc + failed, total_acc + total) - }); - let overall_total = overall_failed + overall_proposed; - - let overall_failure_rate = overall_failed as f64 / overall_total as f64; - let rewards_reduction = rewards_reduction(&overall_failure_rate); - let rewards_percent = ((1.0 - rewards_reduction) * 100.0).round() / 100.0; - - let rewards_stats = RewardsStats { - blocks_failed: overall_failed, - blocks_proposed: overall_proposed, - blocks_total: overall_total, - failure_rate: overall_failure_rate, - rewards_reduction, - }; - - (rewards_percent, rewards_stats) +pub fn compute_rewards_percent(daily_metrics: &[DailyNodeMetrics]) -> RewardsComputationResult { + let mut computation_logger = ComputationLogger::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 overall_failed = computation_logger.execute("Computing Total Failed Blocks", Operation::Sum(daily_failed)); + let overall_proposed = computation_logger.execute("Computing Total Proposed Blocks", Operation::Sum(daily_proposed)); + let overall_total = computation_logger.execute("Computing Total Blocks", Operation::Sum(vec![overall_failed, overall_proposed])); + let overall_failure_rate = computation_logger.execute("Computing Total Failure Rate", Operation::Divide(overall_failed, overall_total)); + let (operations, rewards_reduction) = rewards_reduction_percent(&overall_failure_rate); + computation_logger.add_executed(operations); + let rewards_percent = computation_logger.execute("Total Rewards", Operation::Subtract(dec!(1), rewards_reduction)); + + let computation_input = daily_metrics.iter().map(|metric| metric.to_string()).collect_vec().join("\n"); + + let computation_logger = computation_logger.with_input(computation_input); + + RewardsComputationResult { + // Overflow impossible + rewards_percent: rewards_percent.to_f64().unwrap(), + rewards_reduction: rewards_reduction.to_f64().unwrap(), + + // Overflow impossible since u64 in input will always fit + // No negative numbers + 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(), + computation_log: computation_logger.get_log(), + } } #[cfg(test)] @@ -183,8 +162,8 @@ mod tests { // Overall failed = 130 Overall total = 500 Failure rate = 0.26 // rewards_reduction = 0.266 let daily_metrics: Vec = daily_mocked_metrics(vec![MockedMetrics::new(20, 6, 4), MockedMetrics::new(25, 10, 2)]); - let (rewards_percent, _) = compute_rewards_percent(&daily_metrics); - assert_eq!(rewards_percent, 0.73); + let result = compute_rewards_percent(&daily_metrics); + assert_eq!(result.rewards_percent, 0.7333333333333334); // Overall failed = 45 Overall total = 450 Failure rate = 0.1 // rewards_reduction = 0.0 @@ -192,16 +171,16 @@ mod tests { MockedMetrics::new(1, 400, 20), MockedMetrics::new(1, 5, 25), // no penalty ]); - let (rewards_percent, _) = compute_rewards_percent(&daily_metrics); - assert_eq!(rewards_percent, 1.0); + let result = compute_rewards_percent(&daily_metrics); + assert_eq!(result.rewards_percent, 1.0); // Overall failed = 5 Overall total = 10 Failure rate = 0.5 // rewards_reduction = 0.666 let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(1, 5, 5), // no penalty ]); - let (rewards_percent, _) = compute_rewards_percent(&daily_metrics); - assert_eq!(rewards_percent, 0.33); + let result = compute_rewards_percent(&daily_metrics); + assert_eq!(result.rewards_percent, 0.33333333333333337); } #[test] @@ -209,8 +188,8 @@ mod tests { let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(10, 5, 95), // max failure rate ]); - let (rewards_percent, _) = compute_rewards_percent(&daily_metrics); - assert_eq!(rewards_percent, 0.0); + let result = compute_rewards_percent(&daily_metrics); + assert_eq!(result.rewards_percent, 0.0); } #[test] @@ -218,8 +197,8 @@ mod tests { let daily_metrics: Vec = daily_mocked_metrics(vec![ MockedMetrics::new(10, 9, 1), // min failure rate ]); - let (rewards_percent, _) = compute_rewards_percent(&daily_metrics); - assert_eq!(rewards_percent, 1.0); + let result = compute_rewards_percent(&daily_metrics); + assert_eq!(result.rewards_percent, 1.0); } #[test] @@ -235,15 +214,15 @@ mod tests { let daily_metrics_right_gap: Vec = daily_mocked_metrics(vec![gap.clone(), MockedMetrics::new(1, 6, 4), MockedMetrics::new(1, 7, 3)]); - assert_eq!(compute_rewards_percent(&daily_metrics_mid_gap).0, 0.78); + assert_eq!(compute_rewards_percent(&daily_metrics_mid_gap).rewards_percent, 0.7777777777777779); assert_eq!( - compute_rewards_percent(&daily_metrics_mid_gap).0, - compute_rewards_percent(&daily_metrics_left_gap).0 + compute_rewards_percent(&daily_metrics_mid_gap).rewards_percent, + compute_rewards_percent(&daily_metrics_left_gap).rewards_percent ); assert_eq!( - compute_rewards_percent(&daily_metrics_right_gap).0, - compute_rewards_percent(&daily_metrics_left_gap).0 + compute_rewards_percent(&daily_metrics_right_gap).rewards_percent, + compute_rewards_percent(&daily_metrics_left_gap).rewards_percent ); } @@ -256,11 +235,11 @@ mod tests { ]); let mut daily_metrics = daily_metrics.clone(); - let (rewards_percent, _) = compute_rewards_percent(&daily_metrics); + let result = compute_rewards_percent(&daily_metrics); daily_metrics.reverse(); - let (rewards_percent_rev, _) = compute_rewards_percent(&daily_metrics); + let result_rev = compute_rewards_percent(&daily_metrics); - assert_eq!(rewards_percent, 1.0); - assert_eq!(rewards_percent_rev, rewards_percent); + assert_eq!(result.rewards_percent, 1.0); + assert_eq!(result_rev.rewards_percent, result.rewards_percent); } } diff --git a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/trustworthy-node-metrics.did b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/trustworthy-node-metrics.did index 286fb5fb0..5acac3e98 100644 --- a/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/trustworthy-node-metrics.did +++ b/rs/dre-canisters/trustworthy-node-metrics/src/trustworthy-node-metrics/trustworthy-node-metrics.did @@ -30,19 +30,24 @@ type DailyNodeMetrics = record { }; type RewardsStats = record { + +}; + +type RewardsComputationResult = record { + rewards_percent: float64; + rewards_reduction: float64; blocks_failed: nat64; blocks_proposed: nat64; blocks_total: nat64; failure_rate: float64; - rewards_reduction: float64; + computation_log: text; }; type NodeRewardsResponse = record { node_id: principal; node_provider_id: principal; - rewards_percent: float64; daily_node_metrics: vec DailyNodeMetrics; - rewards_stats: RewardsStats; + rewards_computation: RewardsComputationResult; }; type NodeRewardsArgs = record {