Feature/develop resync (#1844)
* Network-requester: throttle inbound connections (#1789) * Return and handle ClientRequest::LaneQueueLenghts * Pass lane queue lengths to inbound future * Remove unused self reference * Request lane queue lengths periodically for all open connections * Add timeouts * Rename to ConnectionCommandSender and Receiver * Rename to client_connection_tx/rx * Fix wasm build * Replace bool with enum * rust: bump required version to 1.65 in some crates that need it * Add step to release GH actions (#1792) * feat: add a release step to nym contracts GH action * feat: add shrinking the size of wasm * Possibilty to change gateway ws listener (#1779) * add: set gatewayListener * Update types.ts * Update worker.ts * Update contracts-build.yml * real_traffic_stream: reduce frequency of status print (#1794) * Update wallet and connect lock files (#1793) * client-core: add warning when delay multiplier is larger than 1 * Fix decrypting stored received msg (#1786) * Fix decrypting stored received msg * rustfmt * Moving binary message recovery to separate function Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com> * Feature/use expect instead of panicking (#1797) * Implementation of 'Debug' on 'RealMessage' * expect with failed channel name instead of throwing empty panics * Introduced Debug trait constraint in ProxyRunner * Derive Debug for socks5_requests::Message * Make connection_id optional in ClientRequest::Send (#1798) * changelog: add missing entry for fixing message decrypt in gateway-client * websocket-requests: fix length check before deserialize (#1799) * Fix export dkg contract addr (#1800) * Export dkg contract for mainnet when no config file present * Remove redundant env files * nym-cli: improve error reporting/handling and changed `vesting-schedule` queries to use query client instead of signing client * Feature/gateway client protocol version (#1795) * Introducing concept of gateway protocol version * Remove version-based gateway filtering * Fixed the unit test * grammar * Set build on latest release on schedule event * Added nightly build workflow on second latest release * socks5: if any task panics, signal all other tasks to shutdown (#1805) * socks5: signal shutdown on error * Mark as success * Tidy * Reduce wait to 5 sec * Replace unwrap with expect * Two more unwraps * Update changelog * client-core: less frequent status logging (#1806) * Feature/nym connect UI updates (#1784) * create custom titlebar * create help page * create generic modal component * create separate connection time component * link to shipyard docs * move timer to separate component and update connection status component usage * use separate component for copying ip and port details * only show infomodal once after connection * set service provider on tauri side * Emit events when stopped * listen and unlisten for tauri events * connect: add trace log to get_services * Add back CI notifications * Update README Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com> Co-authored-by: Mark Sinclair <14054343+mmsinclair@users.noreply.github.com> * Use default serde value for upgrade (#1807) * fix ui overflow bug (#1808) * update nym connect error text (#1809) * set flag to false * Fix wait_for_signal_and_error on win (#1811) * Add socks5-client changes to nym-connect changelog * Fix links in nym-connect changelog * More entries in nym-connect CHANGELOG * Fix typo in changelog * Update CHANGELOG.md * Experiment/client refactoring (#1814) * experimenting with extracting more common client code * drying up the wasm client * allowing some dead code for the time being * fixed formatting in nym-connect * made socks5 client inside nym-connect immutable * made clippy a bit happier * hidden away target locking for recv timeout * New transactions for increasing amount of pledged tokens * unit tests * Added an option to pledge extra tokens through the vesting contract * Introduced wallet endpoints for new operations * Using updated pledge cap in the vesting contract * Bumping version numbers * Changelog for v1.1.1 * Bumping final version numbers for 1.1.1 * Bumping nym-cli version, missed it last time * socks5-client: SOCKS4a support (#1822) * socks5-client: SOCKS4a support * Tidy * Fix a few errors in socks5 client and network-requester (#1823) * Fix two unwraps in socks5 and network-requester * Make sure client task never sends shutdown signal * Fix panic on getting socks version * wip * connecting to the back and making the requests work * display details modal * logs removal * Feature/pledge more (#1679) * New transactions for increasing amount of pledged tokens * unit tests * Added an option to pledge extra tokens through the vesting contract * Introduced wallet endpoints for new operations * Using updated pledge cap in the vesting contract * Changelog update * nym-connect: update lock file * avoid mix tokens pools * amount error * envs/mainnet: update to latest mixnet contract and nymd validator url * validator-api: add missing shortform for --config-env-file (#1830) * gateway-client: handle shutdown listener (#1829) * WIP * WIP: try another approach * WIP * Reworked * Tidy * fix * validator-api: remove storage dependency in contract cache (#1685) * validator-api: remove storage dependency in contract cache * validator-client: update detailed routes * contract_cache: forward to new endpoints for compat * Move reward_estimate * client: add --no-cover and update --fastmode (#1831) * adding a oversaturaded bonding more modal * common/task: extract out spawn_with_report_error (#1837) * stop panic on failed buffer request * Compilable wasm client * Enabled hard error on lack of gateway-client protocol version * Missing generic parameter for ClientCoreError in BackendError * Removed unused imports * Additional wasm feature locking Co-authored-by: Jon Häggblad <jon.haggblad@gmail.com> Co-authored-by: Fran Arbanas <arbanasfran@gmail.com> Co-authored-by: cgi-bin/ <6095048+sven-hash@users.noreply.github.com> Co-authored-by: Mark Sinclair <14054343+mmsinclair@users.noreply.github.com> Co-authored-by: Bogdan-Ștefan Neacşu <bogdan@nymtech.net> Co-authored-by: Mark Sinclair <mmsinclair@users.noreply.github.com> Co-authored-by: Raphaël Walther <raphael@nymtech.net> Co-authored-by: Fouad <fmtabbara@hotmail.co.uk> Co-authored-by: Gala <calero.vg@gmail.com> Co-authored-by: Dave Hrycyszyn <futurechimp@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
758ba5593c
commit
4578cda0ae
@@ -2,13 +2,11 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::nymd_client::Client;
|
||||
use crate::storage::ValidatorApiStorage;
|
||||
use ::time::OffsetDateTime;
|
||||
use anyhow::Result;
|
||||
use mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use mixnet_contract_common::reward_params::{Performance, RewardingParams};
|
||||
use mixnet_contract_common::{
|
||||
GatewayBond, IdentityKey, Interval, MixId, MixNodeBond, RewardedSetNodeStatus,
|
||||
mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, IdentityKey, Interval,
|
||||
MixId, MixNodeBond, RewardedSetNodeStatus,
|
||||
};
|
||||
use okapi::openapi3::OpenApi;
|
||||
use rocket::fairing::AdHoc;
|
||||
@@ -16,18 +14,17 @@ use rocket::Route;
|
||||
use rocket_okapi::openapi_get_routes_spec;
|
||||
use rocket_okapi::settings::OpenApiSettings;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use task::ShutdownListener;
|
||||
use tokio::sync::{watch, RwLock};
|
||||
use tokio::time;
|
||||
use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus};
|
||||
use validator_api_requests::models::MixnodeStatus;
|
||||
use validator_client::nymd::CosmWasmClient;
|
||||
|
||||
pub(crate) mod reward_estimate;
|
||||
pub(crate) mod routes;
|
||||
|
||||
// The cache can emit notifications to listeners about the current state
|
||||
@@ -42,9 +39,6 @@ pub struct ValidatorCacheRefresher<C> {
|
||||
cache: ValidatorCache,
|
||||
caching_interval: Duration,
|
||||
|
||||
// Readonly: some of the quantities cached depends on values from the storage.
|
||||
storage: Option<ValidatorApiStorage>,
|
||||
|
||||
// Notify listeners that the cache has been updated
|
||||
update_notifier: watch::Sender<CacheNotification>,
|
||||
}
|
||||
@@ -56,14 +50,14 @@ pub struct ValidatorCache {
|
||||
}
|
||||
|
||||
struct ValidatorCacheInner {
|
||||
mixnodes: Cache<Vec<MixNodeBondAnnotated>>,
|
||||
mixnodes: Cache<Vec<MixNodeDetails>>,
|
||||
gateways: Cache<Vec<GatewayBond>>,
|
||||
|
||||
mixnodes_blacklist: Cache<HashSet<MixId>>,
|
||||
gateways_blacklist: Cache<HashSet<IdentityKey>>,
|
||||
|
||||
rewarded_set: Cache<Vec<MixNodeBondAnnotated>>,
|
||||
active_set: Cache<Vec<MixNodeBondAnnotated>>,
|
||||
rewarded_set: Cache<Vec<MixNodeDetails>>,
|
||||
active_set: Cache<Vec<MixNodeDetails>>,
|
||||
|
||||
current_reward_params: Cache<Option<RewardingParams>>,
|
||||
current_interval: Cache<Option<Interval>>,
|
||||
@@ -102,90 +96,33 @@ impl<T: Clone> Cache<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Deref for Cache<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.value
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> ValidatorCacheRefresher<C> {
|
||||
pub(crate) fn new(
|
||||
nymd_client: Client<C>,
|
||||
caching_interval: Duration,
|
||||
cache: ValidatorCache,
|
||||
storage: Option<ValidatorApiStorage>,
|
||||
) -> Self {
|
||||
let (tx, _) = watch::channel(CacheNotification::Start);
|
||||
ValidatorCacheRefresher {
|
||||
nymd_client,
|
||||
cache,
|
||||
caching_interval,
|
||||
storage,
|
||||
update_notifier: tx,
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_performance(&self, mix_id: MixId, epoch: Interval) -> Option<Performance> {
|
||||
self.storage
|
||||
.as_ref()?
|
||||
.get_average_mixnode_uptime_in_the_last_24hrs(
|
||||
mix_id,
|
||||
epoch.current_epoch_end_unix_timestamp(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
pub fn subscribe(&self) -> watch::Receiver<CacheNotification> {
|
||||
self.update_notifier.subscribe()
|
||||
}
|
||||
|
||||
async fn annotate_node_with_details(
|
||||
&self,
|
||||
mixnodes: Vec<MixNodeDetails>,
|
||||
interval_reward_params: RewardingParams,
|
||||
current_interval: Interval,
|
||||
rewarded_set: &HashMap<MixId, RewardedSetNodeStatus>,
|
||||
) -> Vec<MixNodeBondAnnotated> {
|
||||
let mut annotated = Vec::new();
|
||||
for mixnode in mixnodes {
|
||||
let stake_saturation = mixnode
|
||||
.rewarding_details
|
||||
.bond_saturation(&interval_reward_params);
|
||||
|
||||
let uncapped_stake_saturation = mixnode
|
||||
.rewarding_details
|
||||
.uncapped_bond_saturation(&interval_reward_params);
|
||||
|
||||
let performance = self
|
||||
.get_performance(mixnode.mix_id(), current_interval)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).cloned();
|
||||
|
||||
let reward_estimate = reward_estimate::compute_reward_estimate(
|
||||
&mixnode,
|
||||
performance,
|
||||
rewarded_set_status,
|
||||
interval_reward_params,
|
||||
current_interval,
|
||||
);
|
||||
|
||||
let (estimated_operator_apy, estimated_delegators_apy) =
|
||||
reward_estimate::compute_apy_from_reward(
|
||||
&mixnode,
|
||||
reward_estimate,
|
||||
current_interval,
|
||||
);
|
||||
|
||||
annotated.push(MixNodeBondAnnotated {
|
||||
mixnode_details: mixnode,
|
||||
stake_saturation,
|
||||
uncapped_stake_saturation,
|
||||
performance,
|
||||
estimated_operator_apy,
|
||||
estimated_delegators_apy,
|
||||
});
|
||||
}
|
||||
annotated
|
||||
}
|
||||
|
||||
async fn get_rewarded_set_map(&self) -> HashMap<MixId, RewardedSetNodeStatus>
|
||||
where
|
||||
C: CosmWasmClient + Sync + Send,
|
||||
@@ -198,9 +135,9 @@ impl<C> ValidatorCacheRefresher<C> {
|
||||
}
|
||||
|
||||
fn collect_rewarded_and_active_set_details(
|
||||
all_mixnodes: &[MixNodeBondAnnotated],
|
||||
all_mixnodes: &[MixNodeDetails],
|
||||
rewarded_set_nodes: &HashMap<MixId, RewardedSetNodeStatus>,
|
||||
) -> (Vec<MixNodeBondAnnotated>, Vec<MixNodeBondAnnotated>) {
|
||||
) -> (Vec<MixNodeDetails>, Vec<MixNodeDetails>) {
|
||||
let mut active_set = Vec::new();
|
||||
let mut rewarded_set = Vec::new();
|
||||
|
||||
@@ -226,14 +163,10 @@ impl<C> ValidatorCacheRefresher<C> {
|
||||
let mixnodes = self.nymd_client.get_mixnodes().await?;
|
||||
let gateways = self.nymd_client.get_gateways().await?;
|
||||
|
||||
let rewarded_set = self.get_rewarded_set_map().await;
|
||||
|
||||
let mixnodes = self
|
||||
.annotate_node_with_details(mixnodes, rewarding_params, current_interval, &rewarded_set)
|
||||
.await;
|
||||
let rewarded_set_map = self.get_rewarded_set_map().await;
|
||||
|
||||
let (rewarded_set, active_set) =
|
||||
Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set);
|
||||
Self::collect_rewarded_and_active_set_details(&mixnodes, &rewarded_set_map);
|
||||
|
||||
info!(
|
||||
"Updating validator cache. There are {} mixnodes and {} gateways",
|
||||
@@ -324,10 +257,10 @@ impl ValidatorCache {
|
||||
|
||||
async fn update_cache(
|
||||
&self,
|
||||
mixnodes: Vec<MixNodeBondAnnotated>,
|
||||
mixnodes: Vec<MixNodeDetails>,
|
||||
gateways: Vec<GatewayBond>,
|
||||
rewarded_set: Vec<MixNodeBondAnnotated>,
|
||||
active_set: Vec<MixNodeBondAnnotated>,
|
||||
rewarded_set: Vec<MixNodeDetails>,
|
||||
active_set: Vec<MixNodeDetails>,
|
||||
rewarding_params: RewardingParams,
|
||||
current_interval: Interval,
|
||||
) {
|
||||
@@ -422,7 +355,7 @@ impl ValidatorCache {
|
||||
error!("Failed to update gateways blacklist");
|
||||
}
|
||||
|
||||
pub async fn mixnodes_detailed(&self) -> Vec<MixNodeBondAnnotated> {
|
||||
pub async fn mixnodes(&self) -> Vec<MixNodeDetails> {
|
||||
let blacklist = self.mixnodes_blacklist().await;
|
||||
let mixnodes = match time::timeout(Duration::from_millis(100), self.inner.read()).await {
|
||||
Ok(cache) => cache.mixnodes.clone(),
|
||||
@@ -444,14 +377,6 @@ impl ValidatorCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mixnodes(&self) -> Vec<MixNodeDetails> {
|
||||
self.mixnodes_detailed()
|
||||
.await
|
||||
.into_iter()
|
||||
.map(|bond| bond.mixnode_details)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn mixnodes_basic(&self) -> Vec<MixNodeBond> {
|
||||
match time::timeout(Duration::from_millis(100), self.inner.read()).await {
|
||||
Ok(cache) => cache
|
||||
@@ -459,7 +384,7 @@ impl ValidatorCache {
|
||||
.clone()
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
.map(|bond| bond.mixnode_details.bond_information)
|
||||
.map(|bond| bond.bond_information)
|
||||
.collect(),
|
||||
Err(e) => {
|
||||
error!("{}", e);
|
||||
@@ -500,7 +425,7 @@ impl ValidatorCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rewarded_set_detailed(&self) -> Cache<Vec<MixNodeBondAnnotated>> {
|
||||
pub async fn rewarded_set(&self) -> Cache<Vec<MixNodeDetails>> {
|
||||
match time::timeout(Duration::from_millis(100), self.inner.read()).await {
|
||||
Ok(cache) => cache.rewarded_set.clone(),
|
||||
Err(e) => {
|
||||
@@ -510,16 +435,7 @@ impl ValidatorCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn rewarded_set(&self) -> Vec<MixNodeDetails> {
|
||||
self.rewarded_set_detailed()
|
||||
.await
|
||||
.value
|
||||
.into_iter()
|
||||
.map(|bond| bond.mixnode_details)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub async fn active_set_detailed(&self) -> Cache<Vec<MixNodeBondAnnotated>> {
|
||||
pub async fn active_set(&self) -> Cache<Vec<MixNodeDetails>> {
|
||||
match time::timeout(Duration::from_millis(100), self.inner.read()).await {
|
||||
Ok(cache) => cache.active_set.clone(),
|
||||
Err(e) => {
|
||||
@@ -529,15 +445,6 @@ impl ValidatorCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn active_set(&self) -> Vec<MixNodeDetails> {
|
||||
self.active_set_detailed()
|
||||
.await
|
||||
.value
|
||||
.into_iter()
|
||||
.map(|bond| bond.mixnode_details)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) async fn interval_reward_params(&self) -> Cache<Option<RewardingParams>> {
|
||||
match time::timeout(Duration::from_millis(100), self.inner.read()).await {
|
||||
Ok(cache) => cache.current_reward_params.clone(),
|
||||
@@ -558,24 +465,21 @@ impl ValidatorCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn mixnode_details(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> (Option<MixNodeBondAnnotated>, MixnodeStatus) {
|
||||
pub async fn mixnode_details(&self, mix_id: MixId) -> (Option<MixNodeDetails>, 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, < 1000 for active set)
|
||||
|
||||
let active_set = &self.active_set_detailed().await.value;
|
||||
let active_set = &self.active_set().await.value;
|
||||
if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) {
|
||||
return (Some(bond.clone()), MixnodeStatus::Active);
|
||||
}
|
||||
|
||||
let rewarded_set = &self.rewarded_set_detailed().await.value;
|
||||
let rewarded_set = &self.rewarded_set().await.value;
|
||||
if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) {
|
||||
return (Some(bond.clone()), MixnodeStatus::Standby);
|
||||
}
|
||||
|
||||
let all_bonded = &self.mixnodes_detailed().await;
|
||||
let all_bonded = &self.mixnodes().await;
|
||||
if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) {
|
||||
(Some(bond.clone()), MixnodeStatus::Inactive)
|
||||
} else {
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract_cache::ValidatorCache;
|
||||
use mixnet_contract_common::mixnode::MixNodeDetails;
|
||||
use mixnet_contract_common::reward_params::RewardingParams;
|
||||
use mixnet_contract_common::{GatewayBond, Interval, MixId};
|
||||
use rocket::serde::json::Json;
|
||||
use rocket::State;
|
||||
use crate::{
|
||||
contract_cache::ValidatorCache,
|
||||
node_status_api::{
|
||||
helpers::{_get_active_set_detailed, _get_mixnodes_detailed, _get_rewarded_set_detailed},
|
||||
NodeStatusCache,
|
||||
},
|
||||
};
|
||||
use mixnet_contract_common::{
|
||||
mixnode::MixNodeDetails, reward_params::RewardingParams, GatewayBond, Interval, MixId,
|
||||
};
|
||||
use validator_api_requests::models::MixNodeBondAnnotated;
|
||||
|
||||
use rocket::{serde::json::Json, State};
|
||||
use rocket_okapi::openapi;
|
||||
use std::collections::HashSet;
|
||||
use validator_api_requests::models::MixNodeBondAnnotated;
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes")]
|
||||
@@ -17,12 +23,19 @@ pub async fn get_mixnodes(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeDeta
|
||||
Json(cache.mixnodes().await)
|
||||
}
|
||||
|
||||
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
|
||||
// replace this with
|
||||
// ```
|
||||
// pub fn get_mixnodes_detailed() -> Redirect {
|
||||
// Redirect::to(uri!("/v1/status/mixnodes/detailed"))
|
||||
// }
|
||||
// ```
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/detailed")]
|
||||
pub async fn get_mixnodes_detailed(
|
||||
cache: &State<ValidatorCache>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
Json(cache.mixnodes_detailed().await)
|
||||
Json(_get_mixnodes_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
@@ -34,29 +47,43 @@ pub async fn get_gateways(cache: &State<ValidatorCache>) -> Json<Vec<GatewayBond
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/rewarded")]
|
||||
pub async fn get_rewarded_set(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeDetails>> {
|
||||
Json(cache.rewarded_set().await)
|
||||
Json(cache.rewarded_set().await.value)
|
||||
}
|
||||
|
||||
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
|
||||
// replace this with
|
||||
// ```
|
||||
// pub fn get_mixnodes_set_detailed() -> Redirect {
|
||||
// Redirect::to(uri!("/v1/status/mixnodes/rewarded/detailed"))
|
||||
// }
|
||||
// ```
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/rewarded/detailed")]
|
||||
pub async fn get_rewarded_set_detailed(
|
||||
cache: &State<ValidatorCache>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
Json(cache.rewarded_set_detailed().await.value)
|
||||
Json(_get_rewarded_set_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/active")]
|
||||
pub async fn get_active_set(cache: &State<ValidatorCache>) -> Json<Vec<MixNodeDetails>> {
|
||||
Json(cache.active_set().await)
|
||||
Json(cache.active_set().await.value)
|
||||
}
|
||||
|
||||
// DEPRECATED: this endpoint now lives in `node_status_api`. Once all consumers are updated,
|
||||
// replace this with
|
||||
// ```
|
||||
// pub fn get_active_set_detailed() -> Redirect {
|
||||
// Redirect::to(uri!("/status/mixnodes/active/detailed"))
|
||||
// }
|
||||
// ```
|
||||
#[openapi(tag = "contract-cache")]
|
||||
#[get("/mixnodes/active/detailed")]
|
||||
pub async fn get_active_set_detailed(
|
||||
cache: &State<ValidatorCache>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
Json(cache.active_set_detailed().await.value)
|
||||
Json(_get_active_set_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "contract-cache")]
|
||||
|
||||
@@ -156,7 +156,7 @@ impl RewardedSetUpdater {
|
||||
Err(err) => {
|
||||
warn!("failed to obtain the current rewarded set - {}. falling back to the cached version", err);
|
||||
self.validator_cache
|
||||
.rewarded_set_detailed()
|
||||
.rewarded_set()
|
||||
.await
|
||||
.into_inner()
|
||||
.into_iter()
|
||||
|
||||
@@ -121,6 +121,7 @@ fn parse_args() -> ArgMatches {
|
||||
Arg::with_name(CONFIG_ENV_FILE)
|
||||
.help("Path pointing to an env file that configures the validator API")
|
||||
.long(CONFIG_ENV_FILE)
|
||||
.short('c')
|
||||
.takes_value(true)
|
||||
)
|
||||
.arg(
|
||||
@@ -577,7 +578,6 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> {
|
||||
signing_nymd_client.clone(),
|
||||
config.get_caching_interval(),
|
||||
validator_cache.clone(),
|
||||
Some(storage.clone()),
|
||||
);
|
||||
let validator_cache_listener = validator_cache_refresher.subscribe();
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
@@ -599,7 +599,6 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> {
|
||||
nymd_client,
|
||||
config.get_caching_interval(),
|
||||
validator_cache.clone(),
|
||||
None,
|
||||
);
|
||||
let validator_cache_listener = validator_cache_refresher.subscribe();
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
@@ -611,11 +610,13 @@ async fn run_validator_api(matches: ArgMatches) -> Result<()> {
|
||||
// Spawn the node status cache refresher.
|
||||
// It is primarily refreshed in-sync with the validator cache, however provide a fallback
|
||||
// caching interval that is twice the validator cache
|
||||
let storage = rocket.state::<ValidatorApiStorage>().cloned();
|
||||
let mut validator_api_cache_refresher = node_status_api::NodeStatusCacheRefresher::new(
|
||||
node_status_cache,
|
||||
config.get_caching_interval().saturating_mul(2),
|
||||
validator_cache,
|
||||
validator_cache_listener,
|
||||
config.get_caching_interval().saturating_mul(2),
|
||||
storage,
|
||||
);
|
||||
let shutdown_listener = shutdown.subscribe();
|
||||
tokio::spawn(async move { validator_api_cache_refresher.run(shutdown_listener).await });
|
||||
|
||||
@@ -209,7 +209,7 @@ impl PacketSender {
|
||||
ack_sender,
|
||||
fresh_gateway_client_data.gateway_response_timeout,
|
||||
Some(fresh_gateway_client_data.bandwidth_controller.clone()),
|
||||
None,
|
||||
task::ShutdownListener::dummy(),
|
||||
);
|
||||
|
||||
gateway_client
|
||||
|
||||
@@ -2,25 +2,33 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract_cache::{Cache, CacheNotification, ValidatorCache};
|
||||
use contracts_common::truncate_decimal;
|
||||
use mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams};
|
||||
use crate::storage::ValidatorApiStorage;
|
||||
use mixnet_contract_common::reward_params::Performance;
|
||||
use mixnet_contract_common::{
|
||||
Interval, MixId, MixNodeDetails, RewardedSetNodeStatus, RewardingParams,
|
||||
};
|
||||
use rocket::fairing::AdHoc;
|
||||
use serde::Serialize;
|
||||
use std::collections::HashMap;
|
||||
use std::{sync::Arc, time::Duration};
|
||||
use tap::TapFallible;
|
||||
use task::ShutdownListener;
|
||||
use tokio::sync::RwLockReadGuard;
|
||||
use tokio::{
|
||||
sync::{watch, RwLock},
|
||||
time,
|
||||
};
|
||||
use validator_api_requests::models::InclusionProbability;
|
||||
use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus};
|
||||
|
||||
use self::inclusion_probabilities::InclusionProbabilities;
|
||||
|
||||
use super::reward_estimate::{compute_apy_from_reward, compute_reward_estimate};
|
||||
|
||||
mod inclusion_probabilities;
|
||||
|
||||
const CACHE_TIMOUT_MS: u64 = 100;
|
||||
const MAX_SIMULATION_SAMPLES: u64 = 5000;
|
||||
const MAX_SIMULATION_TIME_SEC: u64 = 15;
|
||||
|
||||
enum NodeStatusCacheError {
|
||||
SimulationFailed,
|
||||
SourceDataMissing,
|
||||
}
|
||||
|
||||
// A node status cache suitable for caching values computed in one sweep, such as active set
|
||||
@@ -32,27 +40,16 @@ pub struct NodeStatusCache {
|
||||
inner: Arc<RwLock<NodeStatusCacheInner>>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct NodeStatusCacheInner {
|
||||
mixnodes_annotated: Cache<Vec<MixNodeBondAnnotated>>,
|
||||
rewarded_set_annotated: Cache<Vec<MixNodeBondAnnotated>>,
|
||||
active_set_annotated: Cache<Vec<MixNodeBondAnnotated>>,
|
||||
|
||||
// Estimated active set inclusion probabilities from Monte Carlo simulation
|
||||
inclusion_probabilities: Cache<InclusionProbabilities>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, schemars::JsonSchema)]
|
||||
pub(crate) struct InclusionProbabilities {
|
||||
pub inclusion_probabilities: Vec<InclusionProbability>,
|
||||
pub samples: u64,
|
||||
pub elapsed: Duration,
|
||||
pub delta_max: f64,
|
||||
pub delta_l2: f64,
|
||||
}
|
||||
|
||||
impl InclusionProbabilities {
|
||||
pub fn node(&self, mix_id: MixId) -> Option<&InclusionProbability> {
|
||||
self.inclusion_probabilities
|
||||
.iter()
|
||||
.find(|x| x.mix_id == mix_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeStatusCache {
|
||||
fn new() -> Self {
|
||||
NodeStatusCache {
|
||||
@@ -66,9 +63,18 @@ impl NodeStatusCache {
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_cache(&self, inclusion_probabilities: InclusionProbabilities) {
|
||||
async fn update_cache(
|
||||
&self,
|
||||
mixnodes: Vec<MixNodeBondAnnotated>,
|
||||
rewarded_set: Vec<MixNodeBondAnnotated>,
|
||||
active_set: Vec<MixNodeBondAnnotated>,
|
||||
inclusion_probabilities: InclusionProbabilities,
|
||||
) {
|
||||
match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.write()).await {
|
||||
Ok(mut cache) => {
|
||||
cache.mixnodes_annotated.update(mixnodes);
|
||||
cache.rewarded_set_annotated.update(rewarded_set);
|
||||
cache.active_set_annotated.update(active_set);
|
||||
cache
|
||||
.inclusion_probabilities
|
||||
.update(inclusion_probabilities);
|
||||
@@ -77,45 +83,93 @@ impl NodeStatusCache {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn inclusion_probabilities(&self) -> Option<Cache<InclusionProbabilities>> {
|
||||
async fn get_cache<T>(
|
||||
&self,
|
||||
fn_arg: impl FnOnce(RwLockReadGuard<'_, NodeStatusCacheInner>) -> Cache<T>,
|
||||
) -> Option<Cache<T>> {
|
||||
match time::timeout(Duration::from_millis(CACHE_TIMOUT_MS), self.inner.read()).await {
|
||||
Ok(cache) => Some(cache.inclusion_probabilities.clone()),
|
||||
Ok(cache) => Some(fn_arg(cache)),
|
||||
Err(e) => {
|
||||
error!("{e}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn mixnodes_annotated(&self) -> Option<Cache<Vec<MixNodeBondAnnotated>>> {
|
||||
self.get_cache(|c| c.mixnodes_annotated.clone()).await
|
||||
}
|
||||
|
||||
pub(crate) async fn rewarded_set_annotated(&self) -> Option<Cache<Vec<MixNodeBondAnnotated>>> {
|
||||
self.get_cache(|c| c.rewarded_set_annotated.clone()).await
|
||||
}
|
||||
|
||||
pub(crate) async fn active_set_annotated(&self) -> Option<Cache<Vec<MixNodeBondAnnotated>>> {
|
||||
self.get_cache(|c| c.active_set_annotated.clone()).await
|
||||
}
|
||||
|
||||
pub(crate) async fn inclusion_probabilities(&self) -> Option<Cache<InclusionProbabilities>> {
|
||||
self.get_cache(|c| c.inclusion_probabilities.clone()).await
|
||||
}
|
||||
|
||||
pub async fn mixnode_details(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
) -> (Option<MixNodeBondAnnotated>, 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, < 1000 for active set)
|
||||
|
||||
let active_set = &self.active_set_annotated().await.unwrap().into_inner();
|
||||
if let Some(bond) = active_set.iter().find(|mix| mix.mix_id() == mix_id) {
|
||||
return (Some(bond.clone()), MixnodeStatus::Active);
|
||||
}
|
||||
|
||||
let rewarded_set = &self.rewarded_set_annotated().await.unwrap().into_inner();
|
||||
if let Some(bond) = rewarded_set.iter().find(|mix| mix.mix_id() == mix_id) {
|
||||
return (Some(bond.clone()), MixnodeStatus::Standby);
|
||||
}
|
||||
|
||||
let all_bonded = &self.mixnodes_annotated().await.unwrap().into_inner();
|
||||
if let Some(bond) = all_bonded.iter().find(|mix| mix.mix_id() == mix_id) {
|
||||
(Some(bond.clone()), MixnodeStatus::Inactive)
|
||||
} else {
|
||||
(None, MixnodeStatus::NotFound)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl NodeStatusCacheInner {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
inclusion_probabilities: Default::default(),
|
||||
}
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
// Long running task responsible of keeping the cache up-to-date.
|
||||
pub struct NodeStatusCacheRefresher {
|
||||
// Main stored data
|
||||
cache: NodeStatusCache,
|
||||
fallback_caching_interval: Duration,
|
||||
|
||||
// Sources for when refreshing data
|
||||
contract_cache: ValidatorCache,
|
||||
contract_cache_listener: watch::Receiver<CacheNotification>,
|
||||
fallback_caching_interval: Duration,
|
||||
storage: Option<ValidatorApiStorage>,
|
||||
}
|
||||
|
||||
impl NodeStatusCacheRefresher {
|
||||
pub(crate) fn new(
|
||||
cache: NodeStatusCache,
|
||||
fallback_caching_interval: Duration,
|
||||
contract_cache: ValidatorCache,
|
||||
contract_cache_listener: watch::Receiver<CacheNotification>,
|
||||
fallback_caching_interval: Duration,
|
||||
storage: Option<ValidatorApiStorage>,
|
||||
) -> Self {
|
||||
Self {
|
||||
cache,
|
||||
fallback_caching_interval,
|
||||
contract_cache,
|
||||
contract_cache_listener,
|
||||
fallback_caching_interval,
|
||||
storage,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,78 +230,154 @@ impl NodeStatusCacheRefresher {
|
||||
|
||||
async fn refresh_cache(&self) -> Result<(), NodeStatusCacheError> {
|
||||
log::info!("Updating node status cache");
|
||||
let mixnode_bonds = self.contract_cache.mixnodes().await;
|
||||
let params = self
|
||||
.contract_cache
|
||||
.interval_reward_params()
|
||||
.await
|
||||
.into_inner()
|
||||
.ok_or(NodeStatusCacheError::SimulationFailed)?;
|
||||
let inclusion_probabilities = compute_inclusion_probabilities(&mixnode_bonds, params)
|
||||
.ok_or_else(|| {
|
||||
error!(
|
||||
"Failed to simulate selection probabilties for mixnodes, not updating cache"
|
||||
);
|
||||
NodeStatusCacheError::SimulationFailed
|
||||
})?;
|
||||
|
||||
self.cache.update_cache(inclusion_probabilities).await;
|
||||
// Fetch contract cache data to work with
|
||||
let mixnode_details = self.contract_cache.mixnodes().await;
|
||||
let interval_reward_params = self.contract_cache.interval_reward_params().await;
|
||||
let current_interval = self.contract_cache.current_interval().await;
|
||||
|
||||
let rewarded_set = self.contract_cache.rewarded_set().await;
|
||||
let active_set = self.contract_cache.active_set().await;
|
||||
|
||||
let interval_reward_params =
|
||||
interval_reward_params.ok_or(NodeStatusCacheError::SourceDataMissing)?;
|
||||
let current_interval = current_interval.ok_or(NodeStatusCacheError::SourceDataMissing)?;
|
||||
|
||||
// Compute inclusion probabilities
|
||||
let inclusion_probabilities = InclusionProbabilities::compute(
|
||||
&mixnode_details,
|
||||
interval_reward_params,
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
error!("Failed to simulate selection probabilties for mixnodes, not updating cache");
|
||||
NodeStatusCacheError::SimulationFailed
|
||||
})?;
|
||||
|
||||
// Create annotated data
|
||||
let rewarded_set_node_status = to_rewarded_set_node_status(&rewarded_set, &active_set);
|
||||
let mixnodes_annotated = self
|
||||
.annotate_node_with_details(
|
||||
mixnode_details,
|
||||
interval_reward_params,
|
||||
current_interval,
|
||||
&rewarded_set_node_status,
|
||||
)
|
||||
.await;
|
||||
|
||||
// Create the annotated rewarded and active sets
|
||||
let (rewarded_set, active_set) =
|
||||
split_into_active_and_rewarded_set(&mixnodes_annotated, &rewarded_set_node_status);
|
||||
|
||||
self.cache
|
||||
.update_cache(
|
||||
mixnodes_annotated,
|
||||
rewarded_set,
|
||||
active_set,
|
||||
inclusion_probabilities,
|
||||
)
|
||||
.await;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn get_performance_from_storage(
|
||||
&self,
|
||||
mix_id: MixId,
|
||||
epoch: Interval,
|
||||
) -> Option<Performance> {
|
||||
self.storage
|
||||
.as_ref()?
|
||||
.get_average_mixnode_uptime_in_the_last_24hrs(
|
||||
mix_id,
|
||||
epoch.current_epoch_end_unix_timestamp(),
|
||||
)
|
||||
.await
|
||||
.ok()
|
||||
.map(Into::into)
|
||||
}
|
||||
|
||||
async fn annotate_node_with_details(
|
||||
&self,
|
||||
mixnodes: Vec<MixNodeDetails>,
|
||||
interval_reward_params: RewardingParams,
|
||||
current_interval: Interval,
|
||||
rewarded_set: &HashMap<MixId, RewardedSetNodeStatus>,
|
||||
) -> Vec<MixNodeBondAnnotated> {
|
||||
let mut annotated = Vec::new();
|
||||
for mixnode in mixnodes {
|
||||
let stake_saturation = mixnode
|
||||
.rewarding_details
|
||||
.bond_saturation(&interval_reward_params);
|
||||
|
||||
let uncapped_stake_saturation = mixnode
|
||||
.rewarding_details
|
||||
.uncapped_bond_saturation(&interval_reward_params);
|
||||
|
||||
// If the performance can't be obtained, because the validator-api was not started with
|
||||
// the monitoring (and hence, storage), then reward estimates will be all zero
|
||||
let performance = self
|
||||
.get_performance_from_storage(mixnode.mix_id(), current_interval)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let rewarded_set_status = rewarded_set.get(&mixnode.mix_id()).copied();
|
||||
|
||||
let reward_estimate = compute_reward_estimate(
|
||||
&mixnode,
|
||||
performance,
|
||||
rewarded_set_status,
|
||||
interval_reward_params,
|
||||
current_interval,
|
||||
);
|
||||
|
||||
let (estimated_operator_apy, estimated_delegators_apy) =
|
||||
compute_apy_from_reward(&mixnode, reward_estimate, current_interval);
|
||||
|
||||
annotated.push(MixNodeBondAnnotated {
|
||||
mixnode_details: mixnode,
|
||||
stake_saturation,
|
||||
uncapped_stake_saturation,
|
||||
performance,
|
||||
estimated_operator_apy,
|
||||
estimated_delegators_apy,
|
||||
});
|
||||
}
|
||||
annotated
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inclusion_probabilities(
|
||||
mixnodes: &[MixNodeDetails],
|
||||
params: RewardingParams,
|
||||
) -> Option<InclusionProbabilities> {
|
||||
let active_set_size = params.active_set_size;
|
||||
let standby_set_size = params.rewarded_set_size - active_set_size;
|
||||
|
||||
// Unzip list of total bonds into ids and bonds.
|
||||
// We need to go through this zip/unzip procedure to make sure we have matching identities
|
||||
// for the input to the simulator, which assumes the identity is the position in the vec
|
||||
let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnodes);
|
||||
|
||||
// Compute inclusion probabilitites and keep track of how long time it took.
|
||||
let mut rng = rand::thread_rng();
|
||||
let results = inclusion_probability::simulate_selection_probability_mixnodes(
|
||||
&mixnode_total_bonds,
|
||||
active_set_size as usize,
|
||||
standby_set_size as usize,
|
||||
MAX_SIMULATION_SAMPLES,
|
||||
Duration::from_secs(MAX_SIMULATION_TIME_SEC),
|
||||
&mut rng,
|
||||
)
|
||||
.tap_err(|err| error!("{err}"))
|
||||
.ok()?;
|
||||
|
||||
Some(InclusionProbabilities {
|
||||
inclusion_probabilities: zip_ids_together_with_results(&ids, &results),
|
||||
samples: results.samples,
|
||||
elapsed: results.time,
|
||||
delta_max: results.delta_max,
|
||||
delta_l2: results.delta_l2,
|
||||
})
|
||||
}
|
||||
|
||||
fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec<MixId>, Vec<u128>) {
|
||||
mixnodes
|
||||
fn to_rewarded_set_node_status(
|
||||
rewarded_set: &[MixNodeDetails],
|
||||
active_set: &[MixNodeDetails],
|
||||
) -> HashMap<MixId, RewardedSetNodeStatus> {
|
||||
let mut rewarded_set_node_status: HashMap<MixId, RewardedSetNodeStatus> = rewarded_set
|
||||
.iter()
|
||||
.map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128()))
|
||||
.unzip()
|
||||
.map(|m| (m.mix_id(), RewardedSetNodeStatus::Standby))
|
||||
.collect();
|
||||
for mixnode in active_set {
|
||||
*rewarded_set_node_status
|
||||
.get_mut(&mixnode.mix_id())
|
||||
.expect("All active nodes are rewarded nodes") = RewardedSetNodeStatus::Active;
|
||||
}
|
||||
rewarded_set_node_status
|
||||
}
|
||||
|
||||
fn zip_ids_together_with_results(
|
||||
ids: &[MixId],
|
||||
results: &inclusion_probability::SelectionProbability,
|
||||
) -> Vec<InclusionProbability> {
|
||||
ids.iter()
|
||||
.zip(results.active_set_probability.iter())
|
||||
.zip(results.reserve_set_probability.iter())
|
||||
.map(|((&mix_id, a), r)| InclusionProbability {
|
||||
mix_id,
|
||||
in_active: *a,
|
||||
in_reserve: *r,
|
||||
fn split_into_active_and_rewarded_set(
|
||||
mixnodes_annotated: &[MixNodeBondAnnotated],
|
||||
rewarded_set_node_status: &HashMap<u32, RewardedSetNodeStatus>,
|
||||
) -> (Vec<MixNodeBondAnnotated>, Vec<MixNodeBondAnnotated>) {
|
||||
let rewarded_set: Vec<_> = mixnodes_annotated
|
||||
.iter()
|
||||
.filter(|mixnode| rewarded_set_node_status.get(&mixnode.mix_id()).is_some())
|
||||
.cloned()
|
||||
.collect();
|
||||
let active_set: Vec<_> = rewarded_set
|
||||
.iter()
|
||||
.filter(|mixnode| {
|
||||
rewarded_set_node_status
|
||||
.get(&mixnode.mix_id())
|
||||
.map_or(false, RewardedSetNodeStatus::is_active)
|
||||
})
|
||||
.collect()
|
||||
.cloned()
|
||||
.collect();
|
||||
(rewarded_set, active_set)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
use contracts_common::truncate_decimal;
|
||||
use mixnet_contract_common::{MixId, MixNodeDetails, RewardingParams};
|
||||
use serde::Serialize;
|
||||
use std::time::Duration;
|
||||
use tap::TapFallible;
|
||||
use validator_api_requests::models::InclusionProbability;
|
||||
|
||||
const MAX_SIMULATION_SAMPLES: u64 = 5000;
|
||||
const MAX_SIMULATION_TIME_SEC: u64 = 15;
|
||||
|
||||
#[derive(Clone, Default, Serialize, schemars::JsonSchema)]
|
||||
pub(crate) struct InclusionProbabilities {
|
||||
pub inclusion_probabilities: Vec<InclusionProbability>,
|
||||
pub samples: u64,
|
||||
pub elapsed: Duration,
|
||||
pub delta_max: f64,
|
||||
pub delta_l2: f64,
|
||||
}
|
||||
|
||||
impl InclusionProbabilities {
|
||||
pub(crate) fn compute(
|
||||
mixnodes: &[MixNodeDetails],
|
||||
params: RewardingParams,
|
||||
) -> Option<InclusionProbabilities> {
|
||||
compute_inclusion_probabilities(mixnodes, params)
|
||||
}
|
||||
|
||||
pub(crate) fn node(&self, mix_id: MixId) -> Option<&InclusionProbability> {
|
||||
self.inclusion_probabilities
|
||||
.iter()
|
||||
.find(|x| x.mix_id == mix_id)
|
||||
}
|
||||
}
|
||||
|
||||
fn compute_inclusion_probabilities(
|
||||
mixnodes: &[MixNodeDetails],
|
||||
params: RewardingParams,
|
||||
) -> Option<InclusionProbabilities> {
|
||||
let active_set_size = params.active_set_size;
|
||||
let standby_set_size = params.rewarded_set_size - active_set_size;
|
||||
|
||||
// Unzip list of total bonds into ids and bonds.
|
||||
// We need to go through this zip/unzip procedure to make sure we have matching identities
|
||||
// for the input to the simulator, which assumes the identity is the position in the vec
|
||||
let (ids, mixnode_total_bonds) = unzip_into_mixnode_ids_and_total_bonds(mixnodes);
|
||||
|
||||
// Compute inclusion probabilitites and keep track of how long time it took.
|
||||
let mut rng = rand::thread_rng();
|
||||
let results = inclusion_probability::simulate_selection_probability_mixnodes(
|
||||
&mixnode_total_bonds,
|
||||
active_set_size as usize,
|
||||
standby_set_size as usize,
|
||||
MAX_SIMULATION_SAMPLES,
|
||||
Duration::from_secs(MAX_SIMULATION_TIME_SEC),
|
||||
&mut rng,
|
||||
)
|
||||
.tap_err(|err| error!("{err}"))
|
||||
.ok()?;
|
||||
|
||||
Some(InclusionProbabilities {
|
||||
inclusion_probabilities: zip_ids_together_with_results(&ids, &results),
|
||||
samples: results.samples,
|
||||
elapsed: results.time,
|
||||
delta_max: results.delta_max,
|
||||
delta_l2: results.delta_l2,
|
||||
})
|
||||
}
|
||||
|
||||
fn unzip_into_mixnode_ids_and_total_bonds(mixnodes: &[MixNodeDetails]) -> (Vec<MixId>, Vec<u128>) {
|
||||
mixnodes
|
||||
.iter()
|
||||
.map(|m| (m.mix_id(), truncate_decimal(m.total_stake()).u128()))
|
||||
.unzip()
|
||||
}
|
||||
|
||||
fn zip_ids_together_with_results(
|
||||
ids: &[MixId],
|
||||
results: &inclusion_probability::SelectionProbability,
|
||||
) -> Vec<InclusionProbability> {
|
||||
ids.iter()
|
||||
.zip(results.active_set_probability.iter())
|
||||
.zip(results.reserve_set_probability.iter())
|
||||
.map(|((&mix_id, a), r)| InclusionProbability {
|
||||
mix_id,
|
||||
in_active: *a,
|
||||
in_reserve: *r,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
// Copyright 2021-2022 - Nym Technologies SA <contact@nymtech.net>
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
use crate::contract_cache::reward_estimate::compute_reward_estimate;
|
||||
use crate::contract_cache::Cache;
|
||||
use crate::node_status_api::models::ErrorResponse;
|
||||
use crate::storage::ValidatorApiStorage;
|
||||
@@ -12,11 +11,14 @@ use mixnet_contract_common::{Interval, MixId, RewardedSetNodeStatus};
|
||||
use rocket::http::Status;
|
||||
use rocket::State;
|
||||
use validator_api_requests::models::{
|
||||
ComputeRewardEstParam, InclusionProbabilityResponse, MixnodeCoreStatusResponse,
|
||||
MixnodeStatusReportResponse, MixnodeStatusResponse, MixnodeUptimeHistoryResponse,
|
||||
RewardEstimationResponse, StakeSaturationResponse, UptimeResponse,
|
||||
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, InclusionProbabilityResponse,
|
||||
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse,
|
||||
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse, UptimeResponse,
|
||||
};
|
||||
|
||||
use super::reward_estimate::compute_reward_estimate;
|
||||
|
||||
pub(crate) async fn _mixnode_report(
|
||||
storage: &ValidatorApiStorage,
|
||||
mix_id: MixId,
|
||||
@@ -62,17 +64,18 @@ pub(crate) async fn _get_mixnode_status(
|
||||
}
|
||||
|
||||
pub(crate) async fn _get_mixnode_reward_estimation(
|
||||
cache: &State<ValidatorCache>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
validator_cache: &State<ValidatorCache>,
|
||||
mix_id: MixId,
|
||||
) -> Result<RewardEstimationResponse, ErrorResponse> {
|
||||
let (mixnode, status) = cache.mixnode_details(mix_id).await;
|
||||
if let Some(mixnode) = mixnode {
|
||||
let reward_params = cache.interval_reward_params().await;
|
||||
let reward_params = validator_cache.interval_reward_params().await;
|
||||
let as_at = reward_params.timestamp();
|
||||
let reward_params = reward_params
|
||||
.into_inner()
|
||||
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
|
||||
let current_interval = cache
|
||||
let current_interval = validator_cache
|
||||
.current_interval()
|
||||
.await
|
||||
.into_inner()
|
||||
@@ -117,17 +120,18 @@ async fn average_mixnode_performance(
|
||||
|
||||
pub(crate) async fn _compute_mixnode_reward_estimation(
|
||||
user_reward_param: ComputeRewardEstParam,
|
||||
cache: &ValidatorCache,
|
||||
cache: &NodeStatusCache,
|
||||
validator_cache: &ValidatorCache,
|
||||
mix_id: MixId,
|
||||
) -> Result<RewardEstimationResponse, ErrorResponse> {
|
||||
let (mixnode, actual_status) = cache.mixnode_details(mix_id).await;
|
||||
if let Some(mut mixnode) = mixnode {
|
||||
let reward_params = cache.interval_reward_params().await;
|
||||
let reward_params = validator_cache.interval_reward_params().await;
|
||||
let as_at = reward_params.timestamp();
|
||||
let reward_params = reward_params
|
||||
.into_inner()
|
||||
.ok_or_else(|| ErrorResponse::new("server error", Status::InternalServerError))?;
|
||||
let current_interval = cache
|
||||
let current_interval = validator_cache
|
||||
.current_interval()
|
||||
.await
|
||||
.into_inner()
|
||||
@@ -200,14 +204,15 @@ pub(crate) async fn _compute_mixnode_reward_estimation(
|
||||
}
|
||||
|
||||
pub(crate) async fn _get_mixnode_stake_saturation(
|
||||
cache: &ValidatorCache,
|
||||
cache: &NodeStatusCache,
|
||||
validator_cache: &ValidatorCache,
|
||||
mix_id: MixId,
|
||||
) -> Result<StakeSaturationResponse, ErrorResponse> {
|
||||
let (mixnode, _) = cache.mixnode_details(mix_id).await;
|
||||
if let Some(mixnode) = mixnode {
|
||||
// Recompute the stake saturation just so that we can confidently state that the `as_at`
|
||||
// field is consistent and correct. Luckily this is very cheap.
|
||||
let reward_params = cache.interval_reward_params().await;
|
||||
let reward_params = validator_cache.interval_reward_params().await;
|
||||
let as_at = reward_params.timestamp();
|
||||
let rewarding_params = reward_params
|
||||
.into_inner()
|
||||
@@ -266,3 +271,51 @@ pub(crate) async fn _get_mixnode_avg_uptime(
|
||||
avg_uptime: performance.round_to_integer(),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) async fn _get_mixnode_inclusion_probabilities(
|
||||
cache: &NodeStatusCache,
|
||||
) -> Result<AllInclusionProbabilitiesResponse, ErrorResponse> {
|
||||
if let Some(prob) = cache.inclusion_probabilities().await {
|
||||
let as_at = prob.timestamp();
|
||||
let prob = prob.into_inner();
|
||||
Ok(AllInclusionProbabilitiesResponse {
|
||||
inclusion_probabilities: prob.inclusion_probabilities,
|
||||
samples: prob.samples,
|
||||
elapsed: prob.elapsed,
|
||||
delta_max: prob.delta_max,
|
||||
delta_l2: prob.delta_l2,
|
||||
as_at,
|
||||
})
|
||||
} else {
|
||||
Err(ErrorResponse::new(
|
||||
"No data available".to_string(),
|
||||
Status::ServiceUnavailable,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn _get_mixnodes_detailed(cache: &NodeStatusCache) -> Vec<MixNodeBondAnnotated> {
|
||||
cache
|
||||
.mixnodes_annotated()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
pub(crate) async fn _get_rewarded_set_detailed(
|
||||
cache: &NodeStatusCache,
|
||||
) -> Vec<MixNodeBondAnnotated> {
|
||||
cache
|
||||
.rewarded_set_annotated()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
pub(crate) async fn _get_active_set_detailed(cache: &NodeStatusCache) -> Vec<MixNodeBondAnnotated> {
|
||||
cache
|
||||
.active_set_annotated()
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.into_inner()
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ pub(crate) mod cache;
|
||||
pub(crate) mod helpers;
|
||||
pub(crate) mod local_guard;
|
||||
pub(crate) mod models;
|
||||
pub(crate) mod reward_estimate;
|
||||
pub(crate) mod routes;
|
||||
pub(crate) mod uptime_updater;
|
||||
pub(crate) mod utils;
|
||||
@@ -37,6 +38,9 @@ pub(crate) fn node_status_routes(
|
||||
routes::get_mixnode_inclusion_probability,
|
||||
routes::get_mixnode_avg_uptime,
|
||||
routes::get_mixnode_inclusion_probabilities,
|
||||
routes::get_mixnodes_detailed,
|
||||
routes::get_rewarded_set_detailed,
|
||||
routes::get_active_set_detailed,
|
||||
]
|
||||
} else {
|
||||
// in the minimal variant we would not have access to endpoints relying on existence
|
||||
@@ -46,6 +50,9 @@ pub(crate) fn node_status_routes(
|
||||
routes::get_mixnode_stake_saturation,
|
||||
routes::get_mixnode_inclusion_probability,
|
||||
routes::get_mixnode_inclusion_probabilities,
|
||||
routes::get_mixnodes_detailed,
|
||||
routes::get_rewarded_set_detailed,
|
||||
routes::get_active_set_detailed,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ use mixnet_contract_common::reward_params::{NodeRewardParams, Performance, Rewar
|
||||
use mixnet_contract_common::rewarding::RewardEstimate;
|
||||
use mixnet_contract_common::{Interval, RewardedSetNodeStatus};
|
||||
|
||||
pub fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal {
|
||||
fn compute_apy(epochs_in_year: Decimal, reward: Decimal, pledge_amount: Decimal) -> Decimal {
|
||||
if pledge_amount.is_zero() {
|
||||
return Decimal::zero();
|
||||
}
|
||||
@@ -3,9 +3,10 @@
|
||||
|
||||
use super::NodeStatusCache;
|
||||
use crate::node_status_api::helpers::{
|
||||
_compute_mixnode_reward_estimation, _get_mixnode_avg_uptime,
|
||||
_get_mixnode_inclusion_probability, _get_mixnode_reward_estimation,
|
||||
_get_mixnode_stake_saturation, _get_mixnode_status, _mixnode_core_status_count,
|
||||
_compute_mixnode_reward_estimation, _get_active_set_detailed, _get_mixnode_avg_uptime,
|
||||
_get_mixnode_inclusion_probabilities, _get_mixnode_inclusion_probability,
|
||||
_get_mixnode_reward_estimation, _get_mixnode_stake_saturation, _get_mixnode_status,
|
||||
_get_mixnodes_detailed, _get_rewarded_set_detailed, _mixnode_core_status_count,
|
||||
_mixnode_report, _mixnode_uptime_history,
|
||||
};
|
||||
use crate::node_status_api::models::ErrorResponse;
|
||||
@@ -19,9 +20,9 @@ use rocket_okapi::openapi;
|
||||
use validator_api_requests::models::{
|
||||
AllInclusionProbabilitiesResponse, ComputeRewardEstParam, GatewayCoreStatusResponse,
|
||||
GatewayStatusReportResponse, GatewayUptimeHistoryResponse, InclusionProbabilityResponse,
|
||||
MixnodeCoreStatusResponse, MixnodeStatusReportResponse, MixnodeStatusResponse,
|
||||
MixnodeUptimeHistoryResponse, RewardEstimationResponse, StakeSaturationResponse,
|
||||
UptimeResponse,
|
||||
MixNodeBondAnnotated, MixnodeCoreStatusResponse, MixnodeStatusReportResponse,
|
||||
MixnodeStatusResponse, MixnodeUptimeHistoryResponse, RewardEstimationResponse,
|
||||
StakeSaturationResponse, UptimeResponse,
|
||||
};
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
@@ -112,10 +113,13 @@ pub(crate) async fn get_mixnode_status(
|
||||
#[openapi(tag = "status")]
|
||||
#[get("/mixnode/<mix_id>/reward-estimation")]
|
||||
pub(crate) async fn get_mixnode_reward_estimation(
|
||||
cache: &State<ValidatorCache>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
validator_cache: &State<ValidatorCache>,
|
||||
mix_id: MixId,
|
||||
) -> Result<Json<RewardEstimationResponse>, ErrorResponse> {
|
||||
Ok(Json(_get_mixnode_reward_estimation(cache, mix_id).await?))
|
||||
Ok(Json(
|
||||
_get_mixnode_reward_estimation(cache, validator_cache, mix_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
@@ -125,21 +129,31 @@ pub(crate) async fn get_mixnode_reward_estimation(
|
||||
)]
|
||||
pub(crate) async fn compute_mixnode_reward_estimation(
|
||||
user_reward_param: Json<ComputeRewardEstParam>,
|
||||
cache: &State<ValidatorCache>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
validator_cache: &State<ValidatorCache>,
|
||||
mix_id: MixId,
|
||||
) -> Result<Json<RewardEstimationResponse>, ErrorResponse> {
|
||||
Ok(Json(
|
||||
_compute_mixnode_reward_estimation(user_reward_param.into_inner(), cache, mix_id).await?,
|
||||
_compute_mixnode_reward_estimation(
|
||||
user_reward_param.into_inner(),
|
||||
cache,
|
||||
validator_cache,
|
||||
mix_id,
|
||||
)
|
||||
.await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
#[get("/mixnode/<mix_id>/stake-saturation")]
|
||||
pub(crate) async fn get_mixnode_stake_saturation(
|
||||
cache: &State<ValidatorCache>,
|
||||
cache: &State<NodeStatusCache>,
|
||||
validator_cache: &State<ValidatorCache>,
|
||||
mix_id: MixId,
|
||||
) -> Result<Json<StakeSaturationResponse>, ErrorResponse> {
|
||||
Ok(Json(_get_mixnode_stake_saturation(cache, mix_id).await?))
|
||||
Ok(Json(
|
||||
_get_mixnode_stake_saturation(cache, validator_cache, mix_id).await?,
|
||||
))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
@@ -168,21 +182,29 @@ pub(crate) async fn get_mixnode_avg_uptime(
|
||||
pub(crate) async fn get_mixnode_inclusion_probabilities(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Result<Json<AllInclusionProbabilitiesResponse>, ErrorResponse> {
|
||||
if let Some(prob) = cache.inclusion_probabilities().await {
|
||||
let as_at = prob.timestamp();
|
||||
let prob = prob.into_inner();
|
||||
Ok(Json(AllInclusionProbabilitiesResponse {
|
||||
inclusion_probabilities: prob.inclusion_probabilities,
|
||||
samples: prob.samples,
|
||||
elapsed: prob.elapsed,
|
||||
delta_max: prob.delta_max,
|
||||
delta_l2: prob.delta_l2,
|
||||
as_at,
|
||||
}))
|
||||
} else {
|
||||
Err(ErrorResponse::new(
|
||||
"No data available".to_string(),
|
||||
Status::ServiceUnavailable,
|
||||
))
|
||||
}
|
||||
Ok(Json(_get_mixnode_inclusion_probabilities(cache).await?))
|
||||
}
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
#[get("/mixnodes/detailed")]
|
||||
pub async fn get_mixnodes_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
Json(_get_mixnodes_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
#[get("/mixnodes/rewarded/detailed")]
|
||||
pub async fn get_rewarded_set_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
Json(_get_rewarded_set_detailed(cache).await)
|
||||
}
|
||||
|
||||
#[openapi(tag = "status")]
|
||||
#[get("/mixnodes/active/detailed")]
|
||||
pub async fn get_active_set_detailed(
|
||||
cache: &State<NodeStatusCache>,
|
||||
) -> Json<Vec<MixNodeBondAnnotated>> {
|
||||
Json(_get_active_set_detailed(cache).await)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user