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
This commit is contained in:
Jon Häggblad
2022-06-23 13:42:29 +02:00
committed by GitHub
parent 63692eb30d
commit 3bd21300e0
8 changed files with 173 additions and 42 deletions
+93 -29
View File
@@ -1,7 +1,9 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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<C> {
nymd_client: Client<C>,
cache: ValidatorCache,
caching_interval: Duration,
// Readonly: some of the quantities cached depends on values from the storage.
storage: Option<ValidatorApiStorage>,
}
#[derive(Clone)]
@@ -92,45 +98,96 @@ impl<C> ValidatorCacheRefresher<C> {
nymd_client: Client<C>,
caching_interval: Duration,
cache: ValidatorCache,
storage: Option<ValidatorApiStorage>,
) -> Self {
ValidatorCacheRefresher {
nymd_client,
cache,
caching_interval,
storage,
}
}
fn annotate_bond_with_details(
async fn get_uptime(&self, identity: &IdentityKey, epoch: Interval) -> Option<Uptime> {
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<MixNodeBond>,
interval_reward_params: EpochRewardParams,
current_epoch: Interval,
epochs_in_interval: u64,
current_operator_base_cost: u64,
rewarded_set_identities: &HashMap<IdentityKey, RewardedSetNodeStatus>,
) -> Vec<MixNodeBondAnnotated> {
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<String, RewardedSetNodeStatus>
where
C: CosmWasmClient + Sync,
{
if let Ok(rewarded_set_identities) = self.nymd_client.get_rewarded_set_identities().await {
rewarded_set_identities
.into_iter()
.collect::<HashMap<_, _>>()
} else {
HashMap::new()
}
}
fn collect_rewarded_and_active_set_details(
&self,
all_mixnodes: &[MixNodeBondAnnotated],
rewarded_set_identities: Vec<(IdentityKey, RewardedSetNodeStatus)>,
rewarded_set_identities: &HashMap<IdentityKey, RewardedSetNodeStatus>,
) -> (Vec<MixNodeBondAnnotated>, Vec<MixNodeBondAnnotated>) {
let mut active_set = Vec::new();
let mut rewarded_set = Vec::new();
let rewarded_set_identities = rewarded_set_identities
.into_iter()
.collect::<HashMap<_, _>>();
for mix in all_mixnodes {
if let Some(status) = rewarded_set_identities.get(mix.mixnode_bond.identity()) {
@@ -151,21 +208,28 @@ impl<C> ValidatorCacheRefresher<C> {
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",
@@ -0,0 +1,52 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// 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)
}