From c79ee5052f0fd8326318f6396e9198078c473cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 1 Jun 2022 13:02:16 +0200 Subject: [PATCH] validator-api: add detailed mixnode bond endpoints (#1294) * validator-api: add mixnodes-detailed endpoint * validator-api: add detailed variants of active and rewarded set * explorer-api: cache as mixnode bond detailed * explorer-api: add in stake-saturation in response * changelog: update * rustfmt * validator-api: rename to MixNodeBondResponse * validator-api: cache MixNodeBondResponse instead * validator-api: rename to MixNodeBondAnnotated * validator-client: fix unused warning * explorer-api: remove unnecessary clone * rustfmt --- CHANGELOG.md | 1 + Cargo.lock | 1 + .../validator-client/src/client.rs | 22 +++- .../validator-client/src/validator_api/mod.rs | 44 ++++++- .../src/validator_api/routes.rs | 1 + .../src/country_statistics/geolocate.rs | 16 +-- explorer-api/src/mix_node/http.rs | 16 ++- explorer-api/src/mix_node/models.rs | 1 + explorer-api/src/mix_nodes/models.rs | 39 ++++--- explorer-api/src/ping/http.rs | 2 +- explorer-api/src/state.rs | 4 +- explorer-api/src/tasks.rs | 24 ++-- validator-api/Cargo.toml | 1 + validator-api/src/contract_cache/mod.rs | 108 ++++++++++++++---- validator-api/src/contract_cache/routes.rs | 29 ++++- validator-api/src/node_status_api/routes.rs | 6 +- validator-api/src/rewarded_set_updater/mod.rs | 16 ++- .../validator-api-requests/src/models.rs | 18 ++- 18 files changed, 261 insertions(+), 88 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8334cd702..6548a6200c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Added +- validator-api: add detailed mixnode bond endpoints, and explorer-api makes use of that data to append stake saturation. - wallet: require password to switch accounts - wallet: add simple CLI tool for decrypting and recovering the wallet file. - wallet: added support for multiple accounts ([#1265]) diff --git a/Cargo.lock b/Cargo.lock index 611108642d..f2c1cbf381 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3213,6 +3213,7 @@ dependencies = [ "coconut-interface", "config", "console-subscriber", + "cosmwasm-std", "credential-storage", "credentials", "crypto", diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index adcf4dd2f9..f3f4842013 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -6,12 +6,12 @@ use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, Verifica use mixnet_contract_common::{GatewayBond, IdentityKeyRef, MixNodeBond}; use url::Url; -#[cfg(feature = "nymd-client")] -use validator_api_requests::models::UptimeResponse; use validator_api_requests::models::{ CoreNodeStatusResponse, MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, }; +#[cfg(feature = "nymd-client")] +use validator_api_requests::models::{MixNodeBondAnnotated, UptimeResponse}; #[cfg(feature = "nymd-client")] use network_defaults::DEFAULT_NETWORK; @@ -233,18 +233,36 @@ impl Client { Ok(self.validator_api.get_mixnodes().await?) } + pub async fn get_cached_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_mixnodes_detailed().await?) + } + pub async fn get_cached_rewarded_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.validator_api.get_rewarded_mixnodes().await?) } + pub async fn get_cached_rewarded_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_rewarded_mixnodes_detailed().await?) + } + pub async fn get_cached_active_mixnodes( &self, ) -> Result, ValidatorClientError> { Ok(self.validator_api.get_active_mixnodes().await?) } + pub async fn get_cached_active_mixnodes_detailed( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_active_mixnodes_detailed().await?) + } + pub async fn get_cached_gateways(&self) -> Result, ValidatorClientError> { Ok(self.validator_api.get_gateways().await?) } diff --git a/common/client-libs/validator-client/src/validator_api/mod.rs b/common/client-libs/validator-client/src/validator_api/mod.rs index 4d5463ab3a..66c86f55b6 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -9,8 +9,8 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; use url::Url; use validator_api_requests::models::{ - CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, + CoreNodeStatusResponse, InclusionProbabilityResponse, MixNodeBondAnnotated, + MixnodeStatusResponse, RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; pub mod error; @@ -85,6 +85,16 @@ impl Client { .await } + pub async fn get_mixnodes_detailed( + &self, + ) -> Result, ValidatorAPIError> { + self.query_validator_api( + &[routes::API_VERSION, routes::MIXNODES, routes::DETAILED], + NO_PARAMS, + ) + .await + } + pub async fn get_gateways(&self) -> Result, ValidatorAPIError> { self.query_validator_api(&[routes::API_VERSION, routes::GATEWAYS], NO_PARAMS) .await @@ -98,6 +108,21 @@ impl Client { .await } + pub async fn get_active_mixnodes_detailed( + &self, + ) -> Result, ValidatorAPIError> { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::MIXNODES, + routes::ACTIVE, + routes::DETAILED, + ], + NO_PARAMS, + ) + .await + } + pub async fn get_rewarded_mixnodes(&self) -> Result, ValidatorAPIError> { self.query_validator_api( &[routes::API_VERSION, routes::MIXNODES, routes::REWARDED], @@ -106,6 +131,21 @@ impl Client { .await } + pub async fn get_rewarded_mixnodes_detailed( + &self, + ) -> Result, ValidatorAPIError> { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::MIXNODES, + routes::REWARDED, + routes::DETAILED, + ], + NO_PARAMS, + ) + .await + } + pub async fn get_probs_mixnode_rewarded( &self, mixnode_id: &str, diff --git a/common/client-libs/validator-client/src/validator_api/routes.rs b/common/client-libs/validator-client/src/validator_api/routes.rs index d305e0869b..61c4e309e8 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -7,6 +7,7 @@ pub const API_VERSION: &str = VALIDATOR_API_VERSION; pub const MIXNODES: &str = "mixnodes"; pub const GATEWAYS: &str = "gateways"; +pub const DETAILED: &str = "detailed"; pub const ACTIVE: &str = "active"; pub const REWARDED: &str = "rewarded"; diff --git a/explorer-api/src/country_statistics/geolocate.rs b/explorer-api/src/country_statistics/geolocate.rs index 81e0944dc0..41b7eb6f3a 100644 --- a/explorer-api/src/country_statistics/geolocate.rs +++ b/explorer-api/src/country_statistics/geolocate.rs @@ -51,7 +51,7 @@ impl GeoLocateTask { .state .inner .mixnodes - .is_location_valid(&cache_item.mix_node.identity_key) + .is_location_valid(&cache_item.mix_node().identity_key) .await { // when the cached location is valid, don't locate and continue to next mix node @@ -59,14 +59,14 @@ impl GeoLocateTask { } // the mix node has not been located or is the cache time has expired - match locate(&cache_item.mix_node.host).await { + match locate(&cache_item.mix_node().host).await { Ok(geo_location) => { let location = Location::new(geo_location); trace!( "{} mix nodes already located. Ip {} is located in {:#?}", i, - cache_item.mix_node.host, + cache_item.mix_node().host, location.three_letter_iso_country_code, ); @@ -80,7 +80,7 @@ impl GeoLocateTask { self.state .inner .mixnodes - .set_location(&cache_item.mix_node.identity_key, Some(location)) + .set_location(&cache_item.mix_node().identity_key, Some(location)) .await; // one node has been located, so return out of the loop @@ -89,22 +89,22 @@ impl GeoLocateTask { Err(e) => match e { LocateError::ReqwestError(e) => warn!( "❌ Oh no! Location for {} failed {}", - cache_item.mix_node.host, e + cache_item.mix_node().host, e ), LocateError::NotFound(e) => { warn!( "❌ Location for {} not found. Response body: {}", - cache_item.mix_node.host, e + cache_item.mix_node().host, e ); self.state .inner .mixnodes - .set_location(&cache_item.mix_node.identity_key, None) + .set_location(&cache_item.mix_node().identity_key, None) .await; }, LocateError::RateLimited(e) => warn!( "❌ Oh no, we've been rate limited! Location for {} failed. Response body: {}", - cache_item.mix_node.host, e + cache_item.mix_node().host, e ), }, } diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 2b1946dc83..884deb698e 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -70,8 +70,8 @@ pub(crate) async fn get_description( match state.inner.get_mix_node(pubkey).await { Some(bond) => { match get_mix_node_description( - &bond.mix_node.host, - &bond.mix_node.http_api_port, + &bond.mix_node().host, + &bond.mix_node().http_api_port, ) .await { @@ -87,7 +87,10 @@ pub(crate) async fn get_description( Err(e) => { error!( "Unable to get description for {} on {}:{} -> {}", - pubkey, bond.mix_node.host, bond.mix_node.http_api_port, e + pubkey, + bond.mix_node().host, + bond.mix_node().http_api_port, + e ); Option::None } @@ -114,7 +117,7 @@ pub(crate) async fn get_stats( trace!("No valid cache value for {}", pubkey); match state.inner.get_mix_node(pubkey).await { Some(bond) => { - match get_mix_node_stats(&bond.mix_node.host, &bond.mix_node.http_api_port) + match get_mix_node_stats(&bond.mix_node().host, &bond.mix_node().http_api_port) .await { Ok(response) => { @@ -129,7 +132,10 @@ pub(crate) async fn get_stats( Err(e) => { error!( "Unable to get description for {} on {}:{} -> {}", - pubkey, bond.mix_node.host, bond.mix_node.http_api_port, e + pubkey, + bond.mix_node().host, + bond.mix_node().http_api_port, + e ); Option::None } diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index c038773c71..9fe0bfaf40 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -29,6 +29,7 @@ pub(crate) struct PrettyDetailedMixNodeBond { pub layer: Layer, pub mix_node: MixNode, pub avg_uptime: Option, + pub stake_saturation: f32, } pub(crate) struct MixNodeCache { diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index fb7f57a174..8db58fba0f 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -8,8 +8,7 @@ use std::time::{Duration, SystemTime}; use serde::Serialize; use tokio::sync::RwLock; -use mixnet_contract_common::MixNodeBond; -use validator_client::models::UptimeResponse; +use validator_client::models::{MixNodeBondAnnotated, UptimeResponse}; use crate::cache::Cache; use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; @@ -32,7 +31,7 @@ pub(crate) struct MixNodeSummary { #[derive(Clone, Debug)] pub(crate) struct MixNodesResult { pub(crate) valid_until: SystemTime, - pub(crate) all_mixnodes: HashMap, + pub(crate) all_mixnodes: HashMap, active_mixnodes: HashSet, rewarded_mixnodes: HashSet, } @@ -61,7 +60,7 @@ impl MixNodesResult { self.valid_until >= SystemTime::now() } - fn get_mixnode(&self, pubkey: &str) -> Option { + fn get_mixnode(&self, pubkey: &str) -> Option { if self.is_valid() { self.all_mixnodes.get(pubkey).cloned() } else { @@ -69,7 +68,7 @@ impl MixNodesResult { } } - fn get_mixnodes(&self) -> Option> { + fn get_mixnodes(&self) -> Option> { if self.is_valid() { Some(self.all_mixnodes.clone()) } else { @@ -128,11 +127,11 @@ impl ThreadsafeMixNodesCache { ); } - pub(crate) async fn get_mixnode(&self, pubkey: &str) -> Option { + pub(crate) async fn get_mixnode(&self, pubkey: &str) -> Option { self.mixnodes.read().await.get_mixnode(pubkey) } - pub(crate) async fn get_mixnodes(&self) -> Option> { + pub(crate) async fn get_mixnodes(&self) -> Option> { self.mixnodes.read().await.get_mixnodes() } @@ -151,13 +150,14 @@ impl ThreadsafeMixNodesCache { match bond { Some(bond) => Some(PrettyDetailedMixNodeBond { location: location.and_then(|l| l.location.clone()), - status: mixnodes_guard.determine_node_status(&bond.mix_node.identity_key), - pledge_amount: bond.pledge_amount, - total_delegation: bond.total_delegation, - owner: bond.owner, - layer: bond.layer, - mix_node: bond.mix_node, + status: mixnodes_guard.determine_node_status(&bond.mix_node().identity_key), + pledge_amount: bond.mixnode_bond.pledge_amount, + total_delegation: bond.mixnode_bond.total_delegation, + owner: bond.mixnode_bond.owner, + layer: bond.mixnode_bond.layer, + mix_node: bond.mixnode_bond.mix_node, avg_uptime: health.map(|m| m.avg_uptime), + stake_saturation: bond.stake_saturation, }), None => None, } @@ -172,18 +172,19 @@ impl ThreadsafeMixNodesCache { .all_mixnodes .values() .map(|bond| { - let location = location_guard.get(&bond.mix_node.identity_key); - let copy = bond.clone(); - let health = mixnode_health_guard.get(&bond.mix_node.identity_key); + let location = location_guard.get(&bond.mix_node().identity_key); + let copy = bond.mixnode_bond.clone(); + let health = mixnode_health_guard.get(&bond.mix_node().identity_key); PrettyDetailedMixNodeBond { location: location.and_then(|l| l.location.clone()), - status: mixnodes_guard.determine_node_status(&bond.mix_node.identity_key), + status: mixnodes_guard.determine_node_status(&bond.mix_node().identity_key), pledge_amount: copy.pledge_amount, total_delegation: copy.total_delegation, owner: copy.owner, layer: copy.layer, mix_node: copy.mix_node, avg_uptime: health.map(|m| m.avg_uptime), + stake_saturation: bond.stake_saturation, } }) .collect() @@ -191,14 +192,14 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn update_cache( &self, - all_bonds: Vec, + all_bonds: Vec, rewarded_nodes: HashSet, active_nodes: HashSet, ) { let mut guard = self.mixnodes.write().await; guard.all_mixnodes = all_bonds .into_iter() - .map(|bond| (bond.mix_node.identity_key.to_string(), bond)) + .map(|bond| (bond.mix_node().identity_key.to_string(), bond)) .collect(); guard.rewarded_mixnodes = rewarded_nodes; guard.active_mixnodes = active_nodes; diff --git a/explorer-api/src/ping/http.rs b/explorer-api/src/ping/http.rs index 379084436e..7f451529e3 100644 --- a/explorer-api/src/ping/http.rs +++ b/explorer-api/src/ping/http.rs @@ -42,7 +42,7 @@ pub(crate) async fn index( state.inner.ping.set_pending(pubkey).await; // do the check - let ports = Some(port_check(&bond).await); + let ports = Some(port_check(&bond.mixnode_bond).await); trace!("Tested mix node {}: {:?}", pubkey, ports); let response = PingResponse { ports, diff --git a/explorer-api/src/state.rs b/explorer-api/src/state.rs index 260528def7..1d5a569021 100644 --- a/explorer-api/src/state.rs +++ b/explorer-api/src/state.rs @@ -6,7 +6,7 @@ use log::info; use serde::{Deserialize, Serialize}; use crate::client::ThreadsafeValidatorClient; -use mixnet_contract_common::MixNodeBond; +use validator_client::models::MixNodeBondAnnotated; use crate::country_statistics::country_nodes_distribution::{ CountryNodesDistribution, ThreadsafeCountryNodesDistribution, @@ -35,7 +35,7 @@ pub struct ExplorerApiState { } impl ExplorerApiState { - pub(crate) async fn get_mix_node(&self, pubkey: &str) -> Option { + pub(crate) async fn get_mix_node(&self, pubkey: &str) -> Option { self.mixnodes.get_mixnode(pubkey).await } } diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index e89ca16c62..575e48f314 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -3,8 +3,8 @@ use std::future::Future; -use mixnet_contract_common::{GatewayBond, MixNodeBond}; -use validator_client::models::UptimeResponse; +use mixnet_contract_common::GatewayBond; +use validator_client::models::{MixNodeBondAnnotated, UptimeResponse}; use validator_client::nymd::error::NymdError; use validator_client::nymd::{Paging, QueryNymdClient, ValidatorResponse}; use validator_client::ValidatorClientError; @@ -22,10 +22,10 @@ impl ExplorerApiTasks { } // a helper to remove duplicate code when grabbing active/rewarded/all mixnodes - async fn retrieve_mixnodes<'a, F, Fut>(&'a self, f: F) -> Vec + async fn retrieve_mixnodes<'a, F, Fut>(&'a self, f: F) -> Vec where F: FnOnce(&'a validator_client::Client) -> Fut, - Fut: Future, ValidatorClientError>>, + Fut: Future, ValidatorClientError>>, { let bonds = match f(&self.state.inner.validator_client.0).await { Ok(result) => result, @@ -39,9 +39,9 @@ impl ExplorerApiTasks { bonds } - async fn retrieve_all_mixnodes(&self) -> Vec { + async fn retrieve_all_mixnodes(&self) -> Vec { info!("About to retrieve all mixnode bonds..."); - self.retrieve_mixnodes(validator_client::Client::get_cached_mixnodes) + self.retrieve_mixnodes(validator_client::Client::get_cached_mixnodes_detailed) .await } @@ -77,15 +77,15 @@ impl ExplorerApiTasks { Ok(response) } - async fn retrieve_rewarded_mixnodes(&self) -> Vec { + async fn retrieve_rewarded_mixnodes(&self) -> Vec { info!("About to retrieve rewarded mixnode bonds..."); - self.retrieve_mixnodes(validator_client::Client::get_cached_rewarded_mixnodes) + self.retrieve_mixnodes(validator_client::Client::get_cached_rewarded_mixnodes_detailed) .await } - async fn retrieve_active_mixnodes(&self) -> Vec { + async fn retrieve_active_mixnodes(&self) -> Vec { info!("About to retrieve active mixnode bonds..."); - self.retrieve_mixnodes(validator_client::Client::get_cached_active_mixnodes) + self.retrieve_mixnodes(validator_client::Client::get_cached_active_mixnodes_detailed) .await } @@ -106,13 +106,13 @@ impl ExplorerApiTasks { .retrieve_rewarded_mixnodes() .await .into_iter() - .map(|bond| bond.mix_node.identity_key) + .map(|bond| bond.mix_node().identity_key.clone()) .collect(); let active_nodes = self .retrieve_active_mixnodes() .await .into_iter() - .map(|bond| bond.mix_node.identity_key) + .map(|bond| bond.mix_node().identity_key.clone()) .collect(); self.state .inner diff --git a/validator-api/Cargo.toml b/validator-api/Cargo.toml index 11f5eb275b..ed31e57d8d 100644 --- a/validator-api/Cargo.toml +++ b/validator-api/Cargo.toml @@ -50,6 +50,7 @@ schemars = { version = "0.8", features = ["preserve_order"] } ## internal coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } config = { path = "../common/config" } +cosmwasm-std = "1.0.0-beta8" crypto = { path="../common/crypto" } gateway-client = { path="../common/client-libs/gateway-client" } mixnet-contract-common = { path= "../common/cosmwasm-smart-contracts/mixnet-contract" } diff --git a/validator-api/src/contract_cache/mod.rs b/validator-api/src/contract_cache/mod.rs index 4e51d12400..3ff29e40ce 100644 --- a/validator-api/src/contract_cache/mod.rs +++ b/validator-api/src/contract_cache/mod.rs @@ -22,7 +22,7 @@ use std::sync::Arc; use std::time::Duration; use tokio::sync::RwLock; use tokio::time; -use validator_api_requests::models::MixnodeStatus; +use validator_api_requests::models::{MixNodeBondAnnotated, MixnodeStatus}; use validator_client::nymd::CosmWasmClient; pub(crate) mod routes; @@ -40,14 +40,14 @@ pub struct ValidatorCache { } struct ValidatorCacheInner { - mixnodes: Cache>, + mixnodes: Cache>, gateways: Cache>, mixnodes_blacklist: Cache>, gateways_blacklist: Cache>, - rewarded_set: Cache>, - active_set: Cache>, + rewarded_set: Cache>, + active_set: Cache>, current_reward_params: Cache, current_epoch: Cache>, @@ -99,11 +99,32 @@ impl ValidatorCacheRefresher { } } + fn annotate_bond_with_details( + mixnodes: Vec, + interval_reward_params: EpochRewardParams, + ) -> Vec { + mixnodes + .into_iter() + .map(|mixnode_bond| { + let stake_saturation = mixnode_bond + .stake_saturation( + interval_reward_params.circulating_supply(), + interval_reward_params.rewarded_set_size() as u32, + ) + .to_num(); + MixNodeBondAnnotated { + mixnode_bond, + stake_saturation, + } + }) + .collect() + } + fn collect_rewarded_and_active_set_details( &self, - all_mixnodes: &[MixNodeBond], + all_mixnodes: &[MixNodeBondAnnotated], rewarded_set_identities: Vec<(IdentityKey, RewardedSetNodeStatus)>, - ) -> (Vec, Vec) { + ) -> (Vec, Vec) { let mut active_set = Vec::new(); let mut rewarded_set = Vec::new(); let rewarded_set_identities = rewarded_set_identities @@ -111,7 +132,7 @@ impl ValidatorCacheRefresher { .collect::>(); for mix in all_mixnodes { - if let Some(status) = rewarded_set_identities.get(mix.identity()) { + if let Some(status) = rewarded_set_identities.get(mix.mixnode_bond.identity()) { rewarded_set.push(mix.clone()); if status.is_active() { active_set.push(mix.clone()) @@ -126,11 +147,16 @@ impl ValidatorCacheRefresher { where C: CosmWasmClient + Sync, { + let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?; + let current_epoch = self.nymd_client.get_current_epoch().await?; + 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, active_set) = if let Ok(rewarded_set_identities) = self.nymd_client.get_rewarded_set_identities().await { @@ -139,9 +165,6 @@ impl ValidatorCacheRefresher { (Vec::new(), Vec::new()) }; - let epoch_rewarding_params = self.nymd_client.get_current_epoch_reward_params().await?; - let current_epoch = self.nymd_client.get_current_epoch().await?; - info!( "Updating validator cache. There are {} mixnodes and {} gateways", mixnodes.len(), @@ -184,9 +207,12 @@ impl ValidatorCacheRefresher { pub(crate) fn validator_cache_routes(settings: &OpenApiSettings) -> (Vec, OpenApi) { openapi_get_routes_spec![ settings: routes::get_mixnodes, + routes::get_mixnodes_detailed, routes::get_gateways, routes::get_active_set, + routes::get_active_set_detailed, routes::get_rewarded_set, + routes::get_rewarded_set_detailed, routes::get_blacklisted_mixnodes, routes::get_blacklisted_gateways, routes::get_epoch_reward_params, @@ -210,10 +236,10 @@ impl ValidatorCache { async fn update_cache( &self, - mixnodes: Vec, + mixnodes: Vec, gateways: Vec, - rewarded_set: Vec, - active_set: Vec, + rewarded_set: Vec, + active_set: Vec, epoch_rewarding_params: EpochRewardParams, current_epoch: Interval, ) { @@ -312,7 +338,7 @@ impl ValidatorCache { error!("Failed to update gateways blacklist"); } - pub async fn mixnodes(&self) -> Vec { + pub async fn mixnodes_detailed(&self) -> Vec { let blacklist = self.mixnodes_blacklist().await; let mixnodes = match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.mixnodes.clone(), @@ -326,7 +352,7 @@ impl ValidatorCache { mixnodes .value .iter() - .filter(|mix| !blacklist.value.contains(mix.identity())) + .filter(|mix| !blacklist.value.contains(mix.mixnode_bond.identity())) .cloned() .collect() } else { @@ -334,9 +360,23 @@ impl ValidatorCache { } } + pub async fn mixnodes(&self) -> Vec { + self.mixnodes_detailed() + .await + .into_iter() + .map(|bond| bond.mixnode_bond) + .collect() + } + pub async fn mixnodes_all(&self) -> Vec { match time::timeout(Duration::from_millis(100), self.inner.read()).await { - Ok(cache) => cache.mixnodes.clone().into_inner(), + Ok(cache) => cache + .mixnodes + .clone() + .into_inner() + .into_iter() + .map(|bond| bond.mixnode_bond) + .collect(), Err(e) => { error!("{}", e); Vec::new() @@ -376,7 +416,7 @@ impl ValidatorCache { } } - pub async fn rewarded_set(&self) -> Cache> { + pub async fn rewarded_set_detailed(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.rewarded_set.clone(), Err(e) => { @@ -386,7 +426,16 @@ impl ValidatorCache { } } - pub async fn active_set(&self) -> Cache> { + pub async fn rewarded_set(&self) -> Vec { + self.rewarded_set_detailed() + .await + .value + .into_iter() + .map(|bond| bond.mixnode_bond) + .collect() + } + + pub async fn active_set_detailed(&self) -> Cache> { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.active_set.clone(), Err(e) => { @@ -396,6 +445,15 @@ impl ValidatorCache { } } + pub async fn active_set(&self) -> Vec { + self.active_set_detailed() + .await + .value + .into_iter() + .map(|bond| bond.mixnode_bond) + .collect() + } + pub(crate) async fn epoch_reward_params(&self) -> Cache { match time::timeout(Duration::from_millis(100), self.inner.read()).await { Ok(cache) => cache.current_reward_params.clone(), @@ -419,30 +477,30 @@ impl ValidatorCache { pub async fn mixnode_details( &self, identity: IdentityKeyRef<'_>, - ) -> (Option, MixnodeStatus) { + ) -> (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, < 1000 for active set) - let active_set = &self.active_set().await.value; + let active_set = &self.active_set_detailed().await.value; if let Some(bond) = active_set .iter() - .find(|mix| mix.mix_node.identity_key == identity) + .find(|mix| mix.mix_node().identity_key == identity) { return (Some(bond.clone()), MixnodeStatus::Active); } - let rewarded_set = &self.rewarded_set().await.value; + let rewarded_set = &self.rewarded_set_detailed().await.value; if let Some(bond) = rewarded_set .iter() - .find(|mix| mix.mix_node.identity_key == identity) + .find(|mix| mix.mix_node().identity_key == identity) { return (Some(bond.clone()), MixnodeStatus::Standby); } - let all_bonded = &self.mixnodes().await; + let all_bonded = &self.mixnodes_detailed().await; if let Some(bond) = all_bonded .iter() - .find(|mix| mix.mix_node.identity_key == identity) + .find(|mix| mix.mix_node().identity_key == identity) { (Some(bond.clone()), MixnodeStatus::Inactive) } else { diff --git a/validator-api/src/contract_cache/routes.rs b/validator-api/src/contract_cache/routes.rs index c2d0f075db..2621b7e894 100644 --- a/validator-api/src/contract_cache/routes.rs +++ b/validator-api/src/contract_cache/routes.rs @@ -8,6 +8,7 @@ use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; use std::collections::HashSet; +use validator_api_requests::models::MixNodeBondAnnotated; #[openapi(tag = "contract-cache")] #[get("/mixnodes")] @@ -15,6 +16,14 @@ pub async fn get_mixnodes(cache: &State) -> Json, +) -> Json> { + Json(cache.mixnodes_detailed().await) +} + #[openapi(tag = "contract-cache")] #[get("/gateways")] pub async fn get_gateways(cache: &State) -> Json> { @@ -24,13 +33,29 @@ pub async fn get_gateways(cache: &State) -> Json) -> Json> { - Json(cache.rewarded_set().await.value) + Json(cache.rewarded_set().await) +} + +#[openapi(tag = "contract-cache")] +#[get("/mixnodes/rewarded/detailed")] +pub async fn get_rewarded_set_detailed( + cache: &State, +) -> Json> { + Json(cache.rewarded_set_detailed().await.value) } #[openapi(tag = "contract-cache")] #[get("/mixnodes/active")] pub async fn get_active_set(cache: &State) -> Json> { - Json(cache.active_set().await.value) + Json(cache.active_set().await) +} + +#[openapi(tag = "contract-cache")] +#[get("/mixnodes/active/detailed")] +pub async fn get_active_set_detailed( + cache: &State, +) -> Json> { + Json(cache.active_set_detailed().await.value) } #[openapi(tag = "contract-cache")] diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index a03f59661d..22ccbee404 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -147,7 +147,7 @@ pub(crate) async fn get_mixnode_reward_estimation( let node_reward_params = NodeRewardParams::new(0, uptime.u8() as u128, status.is_active()); let reward_params = RewardParams::new(reward_params, node_reward_params); - match bond.estimate_reward(&reward_params) { + match bond.mixnode_bond.estimate_reward(&reward_params) { Ok(reward_estimate) => { let reponse = RewardEstimationResponse { estimated_total_node_reward: reward_estimate.total_node_reward, @@ -181,11 +181,13 @@ pub(crate) async fn get_mixnode_stake_saturation( ) -> Result, ErrorResponse> { let (bond, _) = cache.mixnode_details(&identity).await; if let Some(bond) = bond { + // Recompute the stake saturation just so that we can confidentaly state that the `as_at` + // field is consistent and correct. Luckily this is very cheap. let interval_reward_params = cache.epoch_reward_params().await; let as_at = interval_reward_params.timestamp(); let interval_reward_params = interval_reward_params.into_inner(); - let saturation = bond.stake_saturation( + let saturation = bond.mixnode_bond.stake_saturation( interval_reward_params.circulating_supply(), interval_reward_params.rewarded_set_size() as u32, ); diff --git a/validator-api/src/rewarded_set_updater/mod.rs b/validator-api/src/rewarded_set_updater/mod.rs index 9f008348d0..d6006d2617 100644 --- a/validator-api/src/rewarded_set_updater/mod.rs +++ b/validator-api/src/rewarded_set_updater/mod.rs @@ -161,21 +161,25 @@ impl RewardedSetUpdater { let epoch = self.epoch().await?; let active_set = self .validator_cache - .active_set() + .active_set_detailed() .await .into_inner() .into_iter() - .map(|bond| bond.mix_node.identity_key) + .map(|bond| bond.mix_node().identity_key.clone()) .collect::>(); - let rewarded_set = self.validator_cache.rewarded_set().await.into_inner(); + let rewarded_set = self + .validator_cache + .rewarded_set_detailed() + .await + .into_inner(); let mut eligible_nodes = Vec::with_capacity(rewarded_set.len()); for rewarded_node in rewarded_set.into_iter() { let uptime = self .storage .get_average_mixnode_uptime_in_the_last_24hrs( - rewarded_node.identity(), + rewarded_node.mixnode_bond.identity(), epoch.end_unix_timestamp(), ) .await?; @@ -183,11 +187,11 @@ impl RewardedSetUpdater { let node_reward_params = NodeRewardParams::new( 0, uptime.u8().into(), - active_set.contains(rewarded_node.identity()), + active_set.contains(rewarded_node.mixnode_bond.identity()), ); eligible_nodes.push(MixnodeToReward { - identity: rewarded_node.identity().clone(), + identity: rewarded_node.mixnode_bond.identity().clone(), params: node_reward_params, }) } diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index d98b5f798d..5998134d9b 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -1,7 +1,7 @@ // Copyright 2022 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use mixnet_contract_common::reward_params::RewardParams; +use mixnet_contract_common::{reward_params::RewardParams, MixNode, MixNodeBond}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt; @@ -53,6 +53,18 @@ pub struct MixnodeStatusResponse { pub status: MixnodeStatus, } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct MixNodeBondAnnotated { + pub mixnode_bond: MixNodeBond, + pub stake_saturation: StakeSaturation, +} + +impl MixNodeBondAnnotated { + pub fn mix_node(&self) -> &MixNode { + &self.mixnode_bond.mix_node + } +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] pub struct RewardEstimationResponse { pub estimated_total_node_reward: u64, @@ -81,10 +93,12 @@ pub struct UptimeResponse { ) )] pub struct StakeSaturationResponse { - pub saturation: f32, + pub saturation: StakeSaturation, pub as_at: i64, } +pub type StakeSaturation = f32; + #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(