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:
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user