From 3bd21300e0b678aaa5aaed06889e343d573d9e12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Thu, 23 Jun 2022 13:42:29 +0200 Subject: [PATCH] validator-api: add apy data to mixnodes endpoint (#1393) * validator-api: compute and return APY for all mixnode bonds * validator-api: tidy * validator-api: handle the absence of storage * validator-api: some comments * validator-api: refinements to apy calc * validator-api: extract out some calculations * changelog: add note --- CHANGELOG.md | 2 + Cargo.lock | 6 +- validator-api/src/contract_cache/mod.rs | 122 +++++++++++++----- .../src/contract_cache/reward_estimate.rs | 52 ++++++++ validator-api/src/main.rs | 22 ++-- validator-api/src/node_status_api/routes.rs | 1 + validator-api/src/nymd_client.rs | 7 + .../validator-api-requests/src/models.rs | 3 + 8 files changed, 173 insertions(+), 42 deletions(-) create mode 100644 validator-api/src/contract_cache/reward_estimate.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index bedacd8e33..ccaa5b2981 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// - validator-api: add detailed mixnode bond endpoints, and explorer-api makes use of that data to append stake saturation - validator-api: add Swagger to document the REST API ([#1249]). - validator-api: Added new endpoints for coconut spending flow and communications with coconut & multisig contracts ([#1261]) +- validator-api: add `uptime`, `estimated_operator_apy`, `estimated_delegators_apy` to `/mixnodes/detailed` endpoint ([#1393]) - network-statistics: a new mixnet service that aggregates and exposes anonymized data about mixnet services ([#1328]) - mixnode: Added basic mixnode hardware reporting to the HTTP API ([#1308]). @@ -53,6 +54,7 @@ Post 1.0.0 release, the changelog format is based on [Keep a Changelog](https:// [#1329]: https://github.com/nymtech/nym/pull/1329 [#1353]: https://github.com/nymtech/nym/pull/1353 [#1376]: https://github.com/nymtech/nym/pull/1376 +[#1393]: https://github.com/nymtech/nym/pull/1393 ## [nym-contracts-v1.0.1](https://github.com/nymtech/nym/tree/nym-contracts-v1.0.1) (2022-06-22) diff --git a/Cargo.lock b/Cargo.lock index eb9b643e0b..861fe09f08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1663,9 +1663,9 @@ dependencies = [ [[package]] name = "fixed" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d3f4dd10ddfcb0bd2b2efe9f18aff37ed48265fda3e20e5d53f046aba9d50a" +checksum = "36a65312835c1097a0c926ff3702df965285fadc33d948b87397ff8961bad881" dependencies = [ "az", "bytemuck", @@ -6346,7 +6346,7 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "vesting-contract" -version = "1.0.0" +version = "1.0.1" dependencies = [ "config", "cosmwasm-std", diff --git a/validator-api/src/contract_cache/mod.rs b/validator-api/src/contract_cache/mod.rs index dc75cafef1..d46b6f0e15 100644 --- a/validator-api/src/contract_cache/mod.rs +++ b/validator-api/src/contract_cache/mod.rs @@ -1,7 +1,9 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 +use crate::node_status_api::models::Uptime; use crate::nymd_client::Client; +use crate::storage::ValidatorApiStorage; use ::time::OffsetDateTime; use anyhow::Result; use mixnet_contract_common::reward_params::EpochRewardParams; @@ -25,12 +27,16 @@ use tokio::time; use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus}; use validator_client::nymd::CosmWasmClient; +pub(crate) mod reward_estimate; pub(crate) mod routes; pub struct ValidatorCacheRefresher { nymd_client: Client, cache: ValidatorCache, caching_interval: Duration, + + // Readonly: some of the quantities cached depends on values from the storage. + storage: Option, } #[derive(Clone)] @@ -92,45 +98,96 @@ impl ValidatorCacheRefresher { nymd_client: Client, caching_interval: Duration, cache: ValidatorCache, + storage: Option, ) -> Self { ValidatorCacheRefresher { nymd_client, cache, caching_interval, + storage, } } - fn annotate_bond_with_details( + async fn get_uptime(&self, identity: &IdentityKey, epoch: Interval) -> Option { + self.storage + .as_ref()? + .get_average_mixnode_uptime_in_the_last_24hrs(identity, epoch.end_unix_timestamp()) + .await + .ok() + } + + async fn annotate_bond_with_details( + &self, mixnodes: Vec, interval_reward_params: EpochRewardParams, + current_epoch: Interval, + epochs_in_interval: u64, + current_operator_base_cost: u64, + rewarded_set_identities: &HashMap, ) -> Vec { - mixnodes - .into_iter() - .map(|mixnode_bond| { - let stake_saturation = mixnode_bond - .stake_saturation( - interval_reward_params.staking_supply(), - interval_reward_params.rewarded_set_size() as u32, - ) - .to_num(); - MixNodeBondAnnotated { - mixnode_bond, - stake_saturation, - } - }) - .collect() + let mut annotated = Vec::new(); + for mixnode_bond in mixnodes { + let stake_saturation = mixnode_bond + .stake_saturation( + interval_reward_params.staking_supply(), + interval_reward_params.rewarded_set_size() as u32, + ) + .to_num(); + + let uptime = self + .get_uptime(mixnode_bond.identity(), current_epoch) + .await + .unwrap_or_default(); + + let is_active = rewarded_set_identities + .get(mixnode_bond.identity()) + .map_or(false, RewardedSetNodeStatus::is_active); + + let reward_estimate = reward_estimate::compute_reward_estimate( + &mixnode_bond, + uptime, + is_active, + interval_reward_params, + current_operator_base_cost, + ); + + let (estimated_operator_apy, estimated_delegators_apy) = + reward_estimate::compute_apy_from_reward( + &mixnode_bond, + reward_estimate, + epochs_in_interval, + ); + + annotated.push(MixNodeBondAnnotated { + mixnode_bond, + stake_saturation, + uptime: uptime.u8(), + estimated_operator_apy, + estimated_delegators_apy, + }); + } + annotated + } + + async fn get_rewarded_set_identities(&self) -> HashMap + where + C: CosmWasmClient + Sync, + { + if let Ok(rewarded_set_identities) = self.nymd_client.get_rewarded_set_identities().await { + rewarded_set_identities + .into_iter() + .collect::>() + } else { + HashMap::new() + } } fn collect_rewarded_and_active_set_details( - &self, all_mixnodes: &[MixNodeBondAnnotated], - rewarded_set_identities: Vec<(IdentityKey, RewardedSetNodeStatus)>, + rewarded_set_identities: &HashMap, ) -> (Vec, Vec) { let mut active_set = Vec::new(); let mut rewarded_set = Vec::new(); - let rewarded_set_identities = rewarded_set_identities - .into_iter() - .collect::>(); for mix in all_mixnodes { if let Some(status) = rewarded_set_identities.get(mix.mixnode_bond.identity()) { @@ -151,21 +208,28 @@ impl ValidatorCacheRefresher { let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?; let current_epoch = self.nymd_client.get_current_epoch().await?; let current_operator_base_cost = self.nymd_client.get_current_operator_cost().await?; + let epochs_in_interval = self.nymd_client.get_epochs_in_interval().await.unwrap_or(0); let (mixnodes, gateways) = tokio::try_join!( self.nymd_client.get_mixnodes(), self.nymd_client.get_gateways(), )?; - let mixnodes = Self::annotate_bond_with_details(mixnodes, epoch_rewarding_params); + let rewarded_set_identities = self.get_rewarded_set_identities().await; - let (rewarded_set, active_set) = if let Ok(rewarded_set_identities) = - self.nymd_client.get_rewarded_set_identities().await - { - self.collect_rewarded_and_active_set_details(&mixnodes, rewarded_set_identities) - } else { - (Vec::new(), Vec::new()) - }; + let mixnodes = self + .annotate_bond_with_details( + mixnodes, + epoch_rewarding_params, + current_epoch, + epochs_in_interval, + current_operator_base_cost, + &rewarded_set_identities, + ) + .await; + + let (rewarded_set, active_set) = + Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set_identities); info!( "Updating validator cache. There are {} mixnodes and {} gateways", diff --git a/validator-api/src/contract_cache/reward_estimate.rs b/validator-api/src/contract_cache/reward_estimate.rs new file mode 100644 index 0000000000..7624853436 --- /dev/null +++ b/validator-api/src/contract_cache/reward_estimate.rs @@ -0,0 +1,52 @@ +// Copyright 2021 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use crate::node_status_api::models::Uptime; +use mixnet_contract_common::mixnode::RewardEstimate; +use mixnet_contract_common::reward_params::{EpochRewardParams, NodeRewardParams, RewardParams}; +use mixnet_contract_common::MixNodeBond; + +pub fn compute_apy(epochs_per_hour: f64, reward: f64, pledge_amount: f64) -> f64 { + epochs_per_hour * 24.0 * 365.0 * 100.0 * reward / pledge_amount +} + +pub fn compute_reward_estimate( + mixnode_bond: &MixNodeBond, + uptime: Uptime, + is_active: bool, + interval_reward_params: EpochRewardParams, + current_operator_base_cost: u64, +) -> RewardEstimate { + let node_reward_params = NodeRewardParams::new(0, u128::from(uptime.u8()), is_active); + let reward_params = RewardParams::new(interval_reward_params, node_reward_params); + + mixnode_bond + .estimate_reward(current_operator_base_cost, &reward_params) + .unwrap_or(RewardEstimate { + total_node_reward: 0, + operator_reward: 0, + delegators_reward: 0, + node_profit: 0, + operator_cost: 0, + }) +} + +pub fn compute_apy_from_reward( + mixnode_bond: &MixNodeBond, + reward_estimate: RewardEstimate, + epochs_in_interval: u64, +) -> (f64, f64) { + let epochs_per_hour = epochs_in_interval as f64 / 720.0; + let pledge = mixnode_bond.pledge_amount().amount.u128(); + let estimated_operator_apy = compute_apy( + epochs_per_hour, + reward_estimate.operator_reward as f64, + pledge as f64, + ); + let estimated_delegators_apy = compute_apy( + epochs_per_hour, + reward_estimate.delegators_reward as f64, + pledge as f64, + ); + (estimated_operator_apy, estimated_delegators_apy) +} diff --git a/validator-api/src/main.rs b/validator-api/src/main.rs index e51a9fd847..f5dc82349e 100644 --- a/validator-api/src/main.rs +++ b/validator-api/src/main.rs @@ -543,25 +543,26 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { // if network monitor is disabled, we're not going to be sending any rewarding hence // we're not starting signing client if config.get_network_monitor_enabled() { + // Main storage + let storage = rocket.state::().unwrap().clone(); + + // setup our daily uptime updater. Note that if network monitor is disabled, then we have + // no data for the updates and hence we don't need to start it up + let uptime_updater = HistoricalUptimeUpdater::new(storage.clone()); + tokio::spawn(async move { uptime_updater.run().await }); + + // spawn the cache refresher let validator_cache_refresher = ValidatorCacheRefresher::new( signing_nymd_client.clone(), config.get_caching_interval(), validator_cache.clone(), + Some(storage.clone()), ); - - // spawn our cacher tokio::spawn(async move { validator_cache_refresher.run().await }); - // setup our daily uptime updater. Note that if network monitor is disabled, then we have - // no data for the updates and hence we don't need to start it up - let storage = rocket.state::().unwrap().clone(); - let uptime_updater = HistoricalUptimeUpdater::new(storage.clone()); - tokio::spawn(async move { uptime_updater.run().await }); - + // spawn rewarded set updater let mut rewarded_set_updater = RewardedSetUpdater::new(signing_nymd_client, validator_cache.clone(), storage).await?; - - // spawn rewarded set updater tokio::spawn(async move { rewarded_set_updater.run().await.unwrap() }); } else { let nymd_client = Client::new_query(&config); @@ -569,6 +570,7 @@ async fn run_validator_api(matches: ArgMatches<'static>) -> Result<()> { nymd_client, config.get_caching_interval(), validator_cache, + None, ); // spawn our cacher diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index dc95fb2db4..b8265c38e8 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -281,6 +281,7 @@ pub(crate) async fn get_mixnode_avg_uptime( })) } +// DEPRECATED: the uptime is available as part of the `/mixnodes/detailed` endpoint #[openapi(tag = "status")] #[get("/mixnodes/avg_uptime")] pub(crate) async fn get_mixnode_avg_uptimes( diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index 380a6f890c..7e5037197a 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -176,6 +176,13 @@ impl Client { self.0.read().await.get_current_epoch().await } + pub(crate) async fn get_epochs_in_interval(&self) -> Result + where + C: CosmWasmClient + Sync, + { + self.0.read().await.get_epochs_in_interval().await + } + pub(crate) async fn get_current_operator_cost(&self) -> Result where C: CosmWasmClient + Sync, diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index 086175dc7b..faf384fdb2 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -51,6 +51,9 @@ pub struct MixnodeStatusResponse { pub struct MixNodeBondAnnotated { pub mixnode_bond: MixNodeBond, pub stake_saturation: StakeSaturation, + pub uptime: u8, + pub estimated_operator_apy: f64, + pub estimated_delegators_apy: f64, } impl MixNodeBondAnnotated {