diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index e53af230ab..59923598ed 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -327,6 +327,11 @@ impl MixNodeBond { self.total_delegation.clone() } + pub fn stake_saturation(&self, circulating_supply: u128, rewarded_set_size: u32) -> U128 { + self.total_bond_to_circulating_supply(circulating_supply) + * U128::from_num(rewarded_set_size) + } + pub fn pledge_to_circulating_supply(&self, circulating_supply: u128) -> U128 { U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(circulating_supply) } diff --git a/validator-api/src/cache/mod.rs b/validator-api/src/cache/mod.rs index b13dd53ae2..4f1063cc35 100644 --- a/validator-api/src/cache/mod.rs +++ b/validator-api/src/cache/mod.rs @@ -2,10 +2,13 @@ // SPDX-License-Identifier: Apache-2.0 use crate::nymd_client::Client; +use crate::rewarding::EpochRewardParams; +use ::time::OffsetDateTime; use anyhow::Result; use config::defaults::VALIDATOR_API_VERSION; use mixnet_contract::{ - ContractStateParams, GatewayBond, IdentityKey, MixNodeBond, RewardingIntervalResponse, + ContractStateParams, GatewayBond, IdentityKey, IdentityKeyRef, MixNodeBond, + RewardingIntervalResponse, }; use rand::prelude::SliceRandom; use rand_chacha::rand_core::SeedableRng; @@ -14,7 +17,7 @@ use rocket::fairing::AdHoc; use serde::Serialize; use std::sync::atomic::{AtomicBool, AtomicU32, AtomicU64, Ordering}; use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use tokio::sync::RwLock; use tokio::time; use validator_client::nymd::hash::SHA256_HASH_SIZE; @@ -22,7 +25,7 @@ use validator_client::nymd::CosmWasmClient; pub(crate) mod routes; -#[derive(Clone, Copy, Debug, Serialize)] +#[derive(Clone, Copy, Debug, Serialize, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub enum MixnodeStatus { Active, // in both the active set and the rewarded set @@ -31,6 +34,12 @@ pub enum MixnodeStatus { NotFound, // doesn't even exist in the bonded set } +impl MixnodeStatus { + pub fn is_active(&self) -> bool { + *self == MixnodeStatus::Active + } +} + pub struct ValidatorCacheRefresher { nymd_client: Client, cache: ValidatorCache, @@ -53,28 +62,40 @@ struct ValidatorCacheInner { current_mixnode_rewarded_set_size: AtomicU32, current_mixnode_active_set_size: AtomicU32, + + current_reward_params: RwLock>, +} + +fn current_unix_timestamp() -> i64 { + let now = OffsetDateTime::now_utc(); + now.unix_timestamp() } #[derive(Default, Serialize, Clone)] pub struct Cache { value: T, - as_at: u64, + as_at: i64, } impl Cache { + fn new(value: T) -> Self { + Cache { + value, + as_at: current_unix_timestamp(), + } + } + fn set(&mut self, value: T) { self.value = value; - self.as_at = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + self.as_at = current_unix_timestamp() } fn renew(&mut self) { - self.as_at = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs() + self.as_at = current_unix_timestamp() + } + + pub fn timestamp(&self) -> i64 { + self.as_at } pub fn into_inner(self) -> T { @@ -113,6 +134,8 @@ impl ValidatorCacheRefresher { ) .await?; + let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?; + info!( "Updating validator cache. There are {} mixnodes and {} gateways", mixnodes.len(), @@ -126,6 +149,7 @@ impl ValidatorCacheRefresher { contract_settings, current_rewarding_interval, rewarding_block_hash, + epoch_rewarding_params, ) .await; @@ -168,7 +192,6 @@ impl ValidatorCache { routes::get_gateways, routes::get_active_mixnodes, routes::get_rewarded_mixnodes, - routes::get_mixnode_status, ], ) }) @@ -237,6 +260,7 @@ impl ValidatorCache { state: ContractStateParams, rewarding_interval: RewardingIntervalResponse, rewarding_block_hash: Option<[u8; SHA256_HASH_SIZE]>, + epoch_rewarding_params: EpochRewardParams, ) { // if the rewarding is currently in progress, don't mess with the rewarded/active sets // as most likely will be changed next time this function is called @@ -282,6 +306,11 @@ impl ValidatorCache { self.inner.mixnodes.write().await.set(mixnodes); self.inner.gateways.write().await.set(gateways); + self.inner + .current_reward_params + .write() + .await + .set(epoch_rewarding_params); } pub async fn mixnodes(&self) -> Cache> { @@ -317,7 +346,14 @@ impl ValidatorCache { } } - pub async fn mixnode_status(&self, identity: IdentityKey) -> MixnodeStatus { + pub(crate) async fn epoch_reward_params(&self) -> Cache { + self.inner.current_reward_params.read().await.clone() + } + + pub async fn mixnode_details( + &self, + identity: IdentityKeyRef<'_>, + ) -> (Option, MixnodeStatus) { // it might not be the most optimal to possibly iterate the entire vector to find (or not) // the relevant value. However, the vectors are relatively small (< 10_000 elements) and // the implementation for active/rewarded sets might change soon so there's no point in premature optimisation @@ -329,35 +365,39 @@ impl ValidatorCache { .load(Ordering::SeqCst) as usize; // see if node is in the top active_set_size of rewarded nodes, i.e. it's active - if rewarded_mixnodes + if let Some(bond) = rewarded_mixnodes .iter() .take(active_set_size) - .any(|mix| mix.mix_node.identity_key == identity) + .find(|mix| mix.mix_node.identity_key == identity) { - MixnodeStatus::Active + (Some(bond.clone()), MixnodeStatus::Active) // see if it's in the bottom part of the rewarded set, i.e. it's in standby - } else if rewarded_mixnodes + } else if let Some(bond) = rewarded_mixnodes .iter() .skip(active_set_size) - .any(|mix| mix.mix_node.identity_key == identity) + .find(|mix| mix.mix_node.identity_key == identity) { - MixnodeStatus::Standby + (Some(bond.clone()), MixnodeStatus::Standby) // if it's not in the rewarded set see if its bonded at all - } else if self + } else if let Some(bond) = self .inner .mixnodes .read() .await .value .iter() - .any(|mix| mix.mix_node.identity_key == identity) + .find(|mix| mix.mix_node.identity_key == identity) { - MixnodeStatus::Inactive + (Some(bond.clone()), MixnodeStatus::Inactive) } else { - MixnodeStatus::NotFound + (None, MixnodeStatus::NotFound) } } + pub async fn mixnode_status(&self, identity: IdentityKey) -> MixnodeStatus { + self.mixnode_details(&identity).await.1 + } + pub fn initialised(&self) -> bool { self.inner.initialised.load(Ordering::Relaxed) } @@ -385,6 +425,7 @@ impl ValidatorCacheInner { rewarded_mixnodes: RwLock::new(Cache::default()), current_mixnode_rewarded_set_size: Default::default(), current_mixnode_active_set_size: Default::default(), + current_reward_params: RwLock::new(Cache::new(EpochRewardParams::new_empty())), } } } diff --git a/validator-api/src/cache/routes.rs b/validator-api/src/cache/routes.rs index 41ebc3c2d9..cf59be469e 100644 --- a/validator-api/src/cache/routes.rs +++ b/validator-api/src/cache/routes.rs @@ -1,11 +1,10 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::cache::{MixnodeStatus, ValidatorCache}; +use crate::cache::ValidatorCache; use mixnet_contract::{GatewayBond, MixNodeBond}; use rocket::serde::json::Json; use rocket::State; -use serde::Serialize; #[get("/mixnodes")] pub(crate) async fn get_mixnodes(cache: &State) -> Json> { @@ -26,18 +25,3 @@ pub(crate) async fn get_rewarded_mixnodes(cache: &State) -> Json pub(crate) async fn get_active_mixnodes(cache: &State) -> Json> { Json(cache.active_mixnodes().await.value) } - -#[derive(Serialize)] -pub(crate) struct MixnodeStatusResponse { - status: MixnodeStatus, -} - -#[get("/mixnode//status")] -pub(crate) async fn get_mixnode_status( - cache: &State, - identity: String, -) -> Json { - Json(MixnodeStatusResponse { - status: cache.mixnode_status(identity).await, - }) -} diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index a74ef82f28..589b06d25f 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -399,6 +399,7 @@ fn expected_monitor_test_runs(config: &Config) -> usize { fn setup_rewarder( config: &Config, + first_epoch: Epoch, rocket: &Rocket, nymd_client: &Client, ) -> Option { @@ -407,10 +408,6 @@ fn setup_rewarder( let node_status_storage = rocket.state::().unwrap().clone(); let validator_cache = rocket.state::().unwrap().clone(); - let first_epoch = Epoch::new( - config.get_first_rewarding_epoch(), - config.get_epoch_length(), - ); Some(Rewarder::new( nymd_client.clone(), validator_cache, @@ -427,11 +424,16 @@ fn setup_rewarder( } } -async fn setup_rocket(config: &Config, liftoff_notify: Arc) -> Result> { +async fn setup_rocket( + config: &Config, + first_epoch: Epoch, + liftoff_notify: Arc, +) -> Result> { // let's build our rocket! let rocket = rocket::build() .attach(setup_cors()?) .attach(setup_liftoff_notify(liftoff_notify)) + .manage(first_epoch) .attach(ValidatorCache::stage()); #[cfg(feature = "coconut")] @@ -440,13 +442,17 @@ async fn setup_rocket(config: &Config, liftoff_notify: Arc) -> Result) -> Result<()> { .map_err(|err| err.into()); } + let first_epoch = Epoch::new( + config.get_first_rewarding_epoch(), + config.get_epoch_length(), + ); + let liftoff_notify = Arc::new(Notify::new()); // let's build our rocket! - let rocket = setup_rocket(&config, Arc::clone(&liftoff_notify)).await?; + let rocket = setup_rocket(&config, first_epoch, Arc::clone(&liftoff_notify)).await?; let monitor_builder = setup_network_monitor(&config, system_version, &rocket); let validator_cache = rocket.state::().unwrap().clone(); @@ -513,7 +524,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { let uptime_updater = HistoricalUptimeUpdater::new(storage); tokio::spawn(async move { uptime_updater.run().await }); - if let Some(rewarder) = setup_rewarder(&config, &rocket, &nymd_client) { + if let Some(rewarder) = setup_rewarder(&config, first_epoch, &rocket, &nymd_client) { info!("Periodic rewarding is starting..."); tokio::spawn(async move { rewarder.run().await }); } else { diff --git a/validator-api/src/node_status_api/mod.rs b/validator-api/src/node_status_api/mod.rs index 3316aec6f4..697fca4403 100644 --- a/validator-api/src/node_status_api/mod.rs +++ b/validator-api/src/node_status_api/mod.rs @@ -1,9 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::storage; use rocket::fairing::AdHoc; -use std::path::PathBuf; use std::time::Duration; pub(crate) mod local_guard; @@ -16,20 +14,35 @@ pub(crate) const FIFTEEN_MINUTES: Duration = Duration::from_secs(900); pub(crate) const ONE_HOUR: Duration = Duration::from_secs(3600); pub(crate) const ONE_DAY: Duration = Duration::from_secs(86400); -pub(crate) fn stage(database_path: PathBuf) -> AdHoc { - AdHoc::on_ignite("SQLx Stage", |rocket| async { - rocket - .attach(storage::ValidatorApiStorage::stage(database_path)) - .mount( - "/v1/status", - routes![ - routes::mixnode_report, - routes::gateway_report, - routes::mixnode_uptime_history, - routes::gateway_uptime_history, - routes::mixnode_core_status_count, - routes::gateway_core_status_count, - ], - ) +pub(crate) fn stage_full() -> AdHoc { + AdHoc::on_ignite("Node Status API Stage", |rocket| async { + rocket.mount( + "/v1/status", + routes![ + routes::mixnode_report, + routes::gateway_report, + routes::mixnode_uptime_history, + routes::gateway_uptime_history, + routes::mixnode_core_status_count, + routes::gateway_core_status_count, + routes::get_mixnode_status, + routes::get_mixnode_reward_estimation, + routes::get_mixnode_stake_saturation, + ], + ) + }) +} + +// in the minimal variant we would not have access to endpoints relying on existence +// of the network monitor and the associated storage +pub(crate) fn stage_minimal() -> AdHoc { + AdHoc::on_ignite("Node Status API Stage", |rocket| async { + rocket.mount( + "/v1/status", + routes![ + routes::get_mixnode_status, + routes::get_mixnode_stake_saturation, + ], + ) }) } diff --git a/validator-api/src/node_status_api/models.rs b/validator-api/src/node_status_api/models.rs index a3fbe6ee63..605e182812 100644 --- a/validator-api/src/node_status_api/models.rs +++ b/validator-api/src/node_status_api/models.rs @@ -1,6 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::cache::MixnodeStatus; use crate::node_status_api::utils::NodeUptimes; use crate::storage::models::NodeStatus; use rocket::http::{ContentType, Status}; @@ -208,22 +209,24 @@ pub struct HistoricalUptime { } pub(crate) struct ErrorResponse { - error: ValidatorApiStorageError, + error_message: String, status: Status, } impl ErrorResponse { - pub(crate) fn new(error: ValidatorApiStorageError, status: Status) -> Self { - ErrorResponse { error, status } + pub(crate) fn new(error_message: impl Into, status: Status) -> Self { + ErrorResponse { + error_message: error_message.into(), + status, + } } } impl<'r, 'o: 'r> Responder<'r, 'o> for ErrorResponse { fn respond_to(self, _: &'r Request<'_>) -> response::Result<'o> { - let message = format!("{}", self.error); Response::build() .header(ContentType::Plain) - .sized_body(message.len(), Cursor::new(message)) + .sized_body(self.error_message.len(), Cursor::new(self.error_message)) .status(self.status) .ok() } @@ -275,3 +278,26 @@ pub struct CoreNodeStatus { pub(crate) identity: String, pub(crate) count: i32, } + +#[derive(Serialize)] +pub(crate) struct MixnodeStatusResponse { + pub(crate) status: MixnodeStatus, +} + +#[derive(Serialize)] +pub(crate) struct RewardEstimationResponse { + pub(crate) estimated_total_node_reward: u128, + pub(crate) estimated_operator_reward: u128, + pub(crate) estimated_delegators_reward: u128, + + pub(crate) current_epoch_start: i64, + pub(crate) current_epoch_end: i64, + pub(crate) current_epoch_uptime: Uptime, + pub(crate) as_at: i64, +} + +#[derive(Serialize)] +pub(crate) struct StakeSaturationResponse { + pub(crate) saturation: f32, + pub(crate) as_at: i64, +} diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index 7b732a5977..2a0d2a6039 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -3,12 +3,14 @@ use crate::node_status_api::models::{ CoreNodeStatus, ErrorResponse, GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, - MixnodeUptimeHistory, + MixnodeStatusResponse, MixnodeUptimeHistory, RewardEstimationResponse, StakeSaturationResponse, }; use crate::storage::ValidatorApiStorage; +use crate::{Epoch, ValidatorCache}; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; +use time::OffsetDateTime; #[get("/mixnode//report")] pub(crate) async fn mixnode_report( @@ -19,7 +21,7 @@ pub(crate) async fn mixnode_report( .construct_mixnode_report(pubkey) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } #[get("/gateway//report")] @@ -31,7 +33,7 @@ pub(crate) async fn gateway_report( .construct_gateway_report(pubkey) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } #[get("/mixnode//history")] @@ -43,7 +45,7 @@ pub(crate) async fn mixnode_uptime_history( .get_mixnode_uptime_history(pubkey) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } #[get("/gateway//history")] @@ -55,7 +57,7 @@ pub(crate) async fn gateway_uptime_history( .get_gateway_uptime_history(pubkey) .await .map(Json) - .map_err(|err| ErrorResponse::new(err, Status::NotFound)) + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound)) } #[get("/mixnode//core-status-count?")] @@ -91,3 +93,84 @@ pub(crate) async fn gateway_core_status_count( count, }) } + +#[get("/mixnode//status")] +pub(crate) async fn get_mixnode_status( + cache: &State, + identity: String, +) -> Json { + Json(MixnodeStatusResponse { + status: cache.mixnode_status(identity).await, + }) +} + +#[get("/mixnode//reward_estimation")] +pub(crate) async fn get_mixnode_reward_estimation( + cache: &State, + storage: &State, + first_epoch: &State, + identity: String, +) -> Result, ErrorResponse> { + let (bond, status) = cache.mixnode_details(&identity).await; + if let Some(bond) = bond { + let epoch_reward_params = cache.epoch_reward_params().await; + let as_at = epoch_reward_params.timestamp(); + let epoch_reward_params = epoch_reward_params.into_inner(); + + let current_epoch = first_epoch.current(OffsetDateTime::now_utc()); + let uptime = storage + .get_average_mixnode_uptime_in_interval( + &identity, + current_epoch.start_unix_timestamp(), + current_epoch.end_unix_timestamp(), + ) + .await + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))?; + + let (estimated_total_node_reward, estimated_operator_reward, estimated_delegators_reward) = + epoch_reward_params.estimate_reward(&bond, uptime.u8(), status.is_active()); + + Ok(Json(RewardEstimationResponse { + estimated_total_node_reward, + estimated_operator_reward, + estimated_delegators_reward, + current_epoch_start: current_epoch.start_unix_timestamp(), + current_epoch_end: current_epoch.end_unix_timestamp(), + current_epoch_uptime: uptime, + as_at, + })) + } else { + Err(ErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) + } +} + +#[get("/mixnode//stake_saturation")] +pub(crate) async fn get_mixnode_stake_saturation( + cache: &State, + identity: String, +) -> Result, ErrorResponse> { + let (bond, _) = cache.mixnode_details(&identity).await; + if let Some(bond) = bond { + let epoch_reward_params = cache.epoch_reward_params().await; + let as_at = epoch_reward_params.timestamp(); + let epoch_reward_params = epoch_reward_params.into_inner(); + + let saturation = bond.stake_saturation( + epoch_reward_params.circulating_supply, + epoch_reward_params.rewarded_set_size, + ); + + Ok(Json(StakeSaturationResponse { + saturation: saturation.to_num(), + as_at, + })) + } else { + Err(ErrorResponse::new( + "mixnode bond not found", + Status::NotFound, + )) + } +} diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index f38503efd7..f74476f6b5 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: Apache-2.0 use crate::config::Config; -use crate::rewarding::{error::RewardingError, MixnodeToReward}; +use crate::rewarding::{error::RewardingError, EpochRewardParams, MixnodeToReward}; use config::defaults::DEFAULT_VALIDATOR_API_PORT; use mixnet_contract::{ ContractStateParams, Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, @@ -98,34 +98,6 @@ impl Client { Ok(time) } - pub(crate) async fn get_reward_pool(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_reward_pool().await?) - } - - pub(crate) async fn get_circulating_supply(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_circulating_supply().await?) - } - - pub(crate) async fn get_sybil_resistance_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_sybil_resistance_percent().await?) - } - - pub(crate) async fn get_epoch_reward_percent(&self) -> Result - where - C: CosmWasmClient + Sync, - { - Ok(self.0.read().await.get_epoch_reward_percent().await?) - } - pub(crate) async fn get_mixnodes(&self) -> Result, ValidatorClientError> where C: CosmWasmClient + Sync, @@ -158,6 +130,31 @@ impl Client { self.0.read().await.get_current_rewarding_interval().await } + pub(crate) async fn get_current_epoch_reward_params( + &self, + ) -> Result + where + C: CosmWasmClient + Sync, + { + let this = self.0.read().await; + + let state = this.get_contract_settings().await?; + let reward_pool = this.get_reward_pool().await?; + let epoch_reward_percent = this.get_epoch_reward_percent().await?; + + let epoch_reward_params = EpochRewardParams { + reward_pool, + circulating_supply: this.get_circulating_supply().await?, + sybil_resistance_percent: this.get_sybil_resistance_percent().await?, + rewarded_set_size: state.mixnode_rewarded_set_size, + active_set_size: state.mixnode_active_set_size, + period_reward_pool: (reward_pool / 100) * epoch_reward_percent as u128, + active_set_work_factor: state.active_set_work_factor, + }; + + Ok(epoch_reward_params) + } + pub(crate) async fn get_rewarding_status( &self, mix_identity: mixnet_contract::IdentityKey, diff --git a/validator-api/src/rewarding/mod.rs b/validator-api/src/rewarding/mod.rs index bccd83e4ee..4e5561e083 100644 --- a/validator-api/src/rewarding/mod.rs +++ b/validator-api/src/rewarding/mod.rs @@ -13,7 +13,9 @@ use crate::storage::ValidatorApiStorage; use config::defaults::DENOM; use log::{error, info}; use mixnet_contract::mixnode::NodeRewardParams; -use mixnet_contract::{ExecuteMsg, IdentityKey, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT}; +use mixnet_contract::{ + ExecuteMsg, IdentityKey, MixNodeBond, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT, +}; use std::convert::TryInto; use std::time::Duration; use time::OffsetDateTime; @@ -23,14 +25,66 @@ use validator_client::nymd::SigningNymdClient; pub(crate) mod epoch; pub(crate) mod error; -struct EpochRewardParams { - reward_pool: u128, - circulating_supply: u128, - sybil_resistance_percent: u8, - rewarded_set_size: u32, - active_set_size: u32, - period_reward_pool: u128, - active_set_work_factor: u8, +#[derive(Copy, Clone)] +pub(crate) struct EpochRewardParams { + pub(crate) reward_pool: u128, + pub(crate) circulating_supply: u128, + pub(crate) sybil_resistance_percent: u8, + pub(crate) rewarded_set_size: u32, + pub(crate) active_set_size: u32, + pub(crate) period_reward_pool: u128, + pub(crate) active_set_work_factor: u8, +} + +impl EpochRewardParams { + // technically it's identical to what would have been derived with a Default implementation, + // however, I prefer to be explicit about it, as a `Default::default` value makes no sense + // apart from the `ValidatorCacheInner` context, where this value is not going to be touched anyway + // (it's guarded behind an `initialised` flag) + pub(crate) fn new_empty() -> Self { + EpochRewardParams { + reward_pool: 0, + circulating_supply: 0, + sybil_resistance_percent: 0, + rewarded_set_size: 0, + active_set_size: 0, + period_reward_pool: 0, + active_set_work_factor: 0, + } + } + + pub(crate) fn estimate_reward( + &self, + node: &MixNodeBond, + uptime: u8, + in_active_set: bool, + ) -> (u128, u128, u128) { + let node_reward_params = NodeRewardParams::new( + self.period_reward_pool, + self.rewarded_set_size.into(), + self.active_set_size.into(), + 0, + self.circulating_supply, + uptime.into(), + self.sybil_resistance_percent, + in_active_set, + self.active_set_work_factor, + ); + + let total_node_reward = node.reward(&node_reward_params); + let operator_reward = node.operator_reward(&node_reward_params); + let delegators_reward = + node.reward_delegation(node.total_delegation().amount, &node_reward_params); + + ( + total_node_reward + .reward() + .checked_to_num() + .unwrap_or_default(), + operator_reward, + delegators_reward, + ) + } } #[derive(Debug, Clone)] @@ -117,24 +171,6 @@ impl Rewarder { } } - async fn epoch_reward_params(&self) -> Result { - let state = self.nymd_client.get_contract_settings().await?; - let reward_pool = self.nymd_client.get_reward_pool().await?; - let epoch_reward_percent = self.nymd_client.get_epoch_reward_percent().await?; - - let epoch_reward_params = EpochRewardParams { - reward_pool, - circulating_supply: self.nymd_client.get_circulating_supply().await?, - sybil_resistance_percent: self.nymd_client.get_sybil_resistance_percent().await?, - rewarded_set_size: state.mixnode_rewarded_set_size, - active_set_size: state.mixnode_active_set_size, - period_reward_pool: (reward_pool / 100) * epoch_reward_percent as u128, - active_set_work_factor: state.active_set_work_factor, - }; - - Ok(epoch_reward_params) - } - /// Obtains the current number of delegators that have delegated their stake towards this particular mixnode. /// /// # Arguments @@ -170,7 +206,7 @@ impl Rewarder { // and the lack of port data / verloc data will eventually be balanced out anyway // by people hesitating to delegate to nodes without them and thus those nodes disappearing // from the active set (once introduced) - let epoch_reward_params = self.epoch_reward_params().await?; + let epoch_reward_params = self.nymd_client.get_current_epoch_reward_params().await?; info!("Rewarding pool stats"); info!(