From 139e89643cb230d376347a283f95a5bad8daf66c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jon=20H=C3=A4ggblad?= Date: Wed, 4 May 2022 11:28:41 +0200 Subject: [PATCH] Endpoints for average mixnode uptime (#1238) --- .../validator-client/src/client.rs | 9 +++ .../validator-client/src/validator_api/mod.rs | 32 ++++++++++- .../src/validator_api/routes.rs | 1 + explorer-api/src/mix_node/models.rs | 1 + explorer-api/src/mix_nodes/models.rs | 26 +++++++++ explorer-api/src/tasks.rs | 30 ++++++++++ validator-api/src/node_status_api/mod.rs | 2 + validator-api/src/node_status_api/routes.rs | 56 ++++++++++++++++++- .../validator-api-requests/src/models.rs | 6 ++ 9 files changed, 161 insertions(+), 2 deletions(-) diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index c784dd709f..adcf4dd2f9 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -5,6 +5,9 @@ use crate::{validator_api, ValidatorClientError}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; 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, @@ -582,6 +585,12 @@ impl Client { Ok(delegations) } + pub async fn get_mixnode_avg_uptimes( + &self, + ) -> Result, ValidatorClientError> { + Ok(self.validator_api.get_mixnode_avg_uptimes().await?) + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, 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 23c760c75f..4d5463ab3a 100644 --- a/common/client-libs/validator-client/src/validator_api/mod.rs +++ b/common/client-libs/validator-client/src/validator_api/mod.rs @@ -10,7 +10,7 @@ use std::collections::HashMap; use url::Url; use validator_api_requests::models::{ CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; pub mod error; @@ -253,6 +253,36 @@ impl Client { .await } + pub async fn get_mixnode_avg_uptime( + &self, + identity: IdentityKeyRef<'_>, + ) -> Result { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODE, + identity, + routes::AVG_UPTIME, + ], + NO_PARAMS, + ) + .await + } + + pub async fn get_mixnode_avg_uptimes(&self) -> Result, ValidatorAPIError> { + self.query_validator_api( + &[ + routes::API_VERSION, + routes::STATUS_ROUTES, + routes::MIXNODES, + routes::AVG_UPTIME, + ], + NO_PARAMS, + ) + .await + } + pub async fn blind_sign( &self, request_body: &BlindSignRequestBody, 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 a21e61188f..d305e0869b 100644 --- a/common/client-libs/validator-client/src/validator_api/routes.rs +++ b/common/client-libs/validator-client/src/validator_api/routes.rs @@ -26,5 +26,6 @@ pub const SINCE_ARG: &str = "since"; pub const STATUS: &str = "status"; pub const REWARD_ESTIMATION: &str = "reward-estimation"; +pub const AVG_UPTIME: &str = "avg_uptime"; pub const STAKE_SATURATION: &str = "stake-saturation"; pub const INCLUSION_CHANCE: &str = "inclusion-probability"; diff --git a/explorer-api/src/mix_node/models.rs b/explorer-api/src/mix_node/models.rs index a85f9eb87f..c038773c71 100644 --- a/explorer-api/src/mix_node/models.rs +++ b/explorer-api/src/mix_node/models.rs @@ -28,6 +28,7 @@ pub(crate) struct PrettyDetailedMixNodeBond { pub owner: Addr, pub layer: Layer, pub mix_node: MixNode, + pub avg_uptime: Option, } pub(crate) struct MixNodeCache { diff --git a/explorer-api/src/mix_nodes/models.rs b/explorer-api/src/mix_nodes/models.rs index bd2ebe4fe8..fb7f57a174 100644 --- a/explorer-api/src/mix_nodes/models.rs +++ b/explorer-api/src/mix_nodes/models.rs @@ -9,7 +9,9 @@ use serde::Serialize; use tokio::sync::RwLock; use mixnet_contract_common::MixNodeBond; +use validator_client::models::UptimeResponse; +use crate::cache::Cache; use crate::mix_node::models::{MixnodeStatus, PrettyDetailedMixNodeBond}; use crate::mix_nodes::location::{Location, LocationCache, LocationCacheItem}; use crate::mix_nodes::CACHE_ENTRY_TTL; @@ -76,10 +78,16 @@ impl MixNodesResult { } } +#[derive(Clone, Debug)] +pub(crate) struct MixNodeHealth { + avg_uptime: u8, +} + #[derive(Clone)] pub(crate) struct ThreadsafeMixNodesCache { mixnodes: Arc>, locations: Arc>, + mixnode_health: Arc>>, } impl ThreadsafeMixNodesCache { @@ -87,6 +95,7 @@ impl ThreadsafeMixNodesCache { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), locations: Arc::new(RwLock::new(LocationCache::new())), + mixnode_health: Arc::new(RwLock::new(Cache::new())), } } @@ -94,6 +103,7 @@ impl ThreadsafeMixNodesCache { ThreadsafeMixNodesCache { mixnodes: Arc::new(RwLock::new(MixNodesResult::new())), locations: Arc::new(RwLock::new(locations)), + mixnode_health: Arc::new(RwLock::new(Cache::new())), } } @@ -132,9 +142,11 @@ impl ThreadsafeMixNodesCache { ) -> Option { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; + let mixnode_health_guard = self.mixnode_health.read().await; let bond = mixnodes_guard.get_mixnode(identity_key); let location = location_guard.get(identity_key); + let health = mixnode_health_guard.get(identity_key); match bond { Some(bond) => Some(PrettyDetailedMixNodeBond { @@ -145,6 +157,7 @@ impl ThreadsafeMixNodesCache { owner: bond.owner, layer: bond.layer, mix_node: bond.mix_node, + avg_uptime: health.map(|m| m.avg_uptime), }), None => None, } @@ -153,6 +166,7 @@ impl ThreadsafeMixNodesCache { pub(crate) async fn get_detailed_mixnodes(&self) -> Vec { let mixnodes_guard = self.mixnodes.read().await; let location_guard = self.locations.read().await; + let mixnode_health_guard = self.mixnode_health.read().await; mixnodes_guard .all_mixnodes @@ -160,6 +174,7 @@ impl ThreadsafeMixNodesCache { .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); PrettyDetailedMixNodeBond { location: location.and_then(|l| l.location.clone()), status: mixnodes_guard.determine_node_status(&bond.mix_node.identity_key), @@ -168,6 +183,7 @@ impl ThreadsafeMixNodesCache { owner: copy.owner, layer: copy.layer, mix_node: copy.mix_node, + avg_uptime: health.map(|m| m.avg_uptime), } }) .collect() @@ -188,4 +204,14 @@ impl ThreadsafeMixNodesCache { guard.active_mixnodes = active_nodes; guard.valid_until = SystemTime::now() + CACHE_ENTRY_TTL; } + + pub(crate) async fn update_health_cache(&self, all_uptimes: Vec) { + let mut mixnode_health = self.mixnode_health.write().await; + for uptime in all_uptimes { + let health = MixNodeHealth { + avg_uptime: uptime.avg_uptime, + }; + mixnode_health.set(&uptime.identity, health); + } + } } diff --git a/explorer-api/src/tasks.rs b/explorer-api/src/tasks.rs index 1f59507cd7..e89ca16c62 100644 --- a/explorer-api/src/tasks.rs +++ b/explorer-api/src/tasks.rs @@ -4,6 +4,7 @@ use std::future::Future; use mixnet_contract_common::{GatewayBond, MixNodeBond}; +use validator_client::models::UptimeResponse; use validator_client::nymd::error::NymdError; use validator_client::nymd::{Paging, QueryNymdClient, ValidatorResponse}; use validator_client::ValidatorClientError; @@ -88,6 +89,17 @@ impl ExplorerApiTasks { .await } + async fn retrieve_all_mixnode_avg_uptimes( + &self, + ) -> Result, ValidatorClientError> { + self.state + .inner + .validator_client + .0 + .get_mixnode_avg_uptimes() + .await + } + async fn update_mixnode_cache(&self) { let all_bonds = self.retrieve_all_mixnodes().await; let rewarded_nodes = self @@ -109,6 +121,21 @@ impl ExplorerApiTasks { .await; } + async fn update_mixnode_health_cache(&self) { + match self.retrieve_all_mixnode_avg_uptimes().await { + Ok(response) => { + self.state + .inner + .mixnodes + .update_health_cache(response) + .await + } + Err(e) => { + error!("Failed to get mixnode avg uptimes: {:?}", e) + } + } + } + async fn update_validators_cache(&self) { match self.retrieve_all_validators().await { Ok(response) => self.state.inner.validators.update_cache(response).await, @@ -145,6 +172,9 @@ impl ExplorerApiTasks { info!("Updating mix node cache..."); self.update_mixnode_cache().await; + + info!("Updating mix node health cache..."); + self.update_mixnode_health_cache().await; info!("Done"); } }); diff --git a/validator-api/src/node_status_api/mod.rs b/validator-api/src/node_status_api/mod.rs index 8c81bd45fc..9802f7c989 100644 --- a/validator-api/src/node_status_api/mod.rs +++ b/validator-api/src/node_status_api/mod.rs @@ -30,6 +30,8 @@ pub(crate) fn stage_full() -> AdHoc { routes::get_mixnode_reward_estimation, routes::get_mixnode_stake_saturation, routes::get_mixnode_inclusion_probability, + routes::get_mixnode_avg_uptime, + routes::get_mixnode_avg_uptimes, ], ) }) diff --git a/validator-api/src/node_status_api/routes.rs b/validator-api/src/node_status_api/routes.rs index e8dcb0a23f..3490828f06 100644 --- a/validator-api/src/node_status_api/routes.rs +++ b/validator-api/src/node_status_api/routes.rs @@ -8,13 +8,14 @@ use crate::node_status_api::models::{ use crate::storage::ValidatorApiStorage; use crate::ValidatorCache; use mixnet_contract_common::reward_params::{NodeRewardParams, RewardParams}; +use mixnet_contract_common::Interval; use rocket::http::Status; use rocket::serde::json::Json; use rocket::State; use rocket_okapi::openapi; use validator_api_requests::models::{ CoreNodeStatusResponse, InclusionProbabilityResponse, MixnodeStatusResponse, - RewardEstimationResponse, StakeSaturationResponse, + RewardEstimationResponse, StakeSaturationResponse, UptimeResponse, }; use super::models::Uptime; @@ -237,3 +238,56 @@ pub(crate) async fn get_mixnode_inclusion_probability( Json(None) } } + +async fn average_mixnode_uptime( + identity: &str, + current_epoch: Option, + storage: &State, +) -> Result { + Ok(if let Some(epoch) = current_epoch { + storage + .get_average_mixnode_uptime_in_the_last_24hrs(identity, epoch.end_unix_timestamp()) + .await + .map_err(|err| ErrorResponse::new(err.to_string(), Status::NotFound))? + } else { + Uptime::default() + }) +} + +#[openapi(tag = "mixnode")] +#[get("/mixnode//avg_uptime")] +pub(crate) async fn get_mixnode_avg_uptime( + cache: &State, + storage: &State, + identity: String, +) -> Result, ErrorResponse> { + let current_epoch = cache.current_epoch().await.into_inner(); + let uptime = average_mixnode_uptime(&identity, current_epoch, storage).await?; + + Ok(Json(UptimeResponse { + identity, + avg_uptime: uptime.u8(), + })) +} + +#[openapi(tag = "mixnode")] +#[get("/mixnodes/avg_uptime")] +pub(crate) async fn get_mixnode_avg_uptimes( + cache: &State, + storage: &State, +) -> Result>, ErrorResponse> { + let mixnodes = cache.mixnodes().await; + let current_epoch = cache.current_epoch().await.into_inner(); + + let mut response = Vec::new(); + for mixnode in mixnodes { + let uptime = average_mixnode_uptime(mixnode.identity(), current_epoch, storage).await?; + + response.push(UptimeResponse { + identity: mixnode.identity().to_string(), + avg_uptime: uptime.u8(), + }) + } + + Ok(Json(response)) +} diff --git a/validator-api/validator-api-requests/src/models.rs b/validator-api/validator-api-requests/src/models.rs index ba8f5da923..46e849f816 100644 --- a/validator-api/validator-api-requests/src/models.rs +++ b/validator-api/validator-api-requests/src/models.rs @@ -63,6 +63,12 @@ pub struct RewardEstimationResponse { pub as_at: i64, } +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +pub struct UptimeResponse { + pub identity: String, + pub avg_uptime: u8, +} + #[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema)] #[cfg_attr(test, derive(ts_rs::TS))] #[cfg_attr(