basic handlers with a lot of repeated code

This commit is contained in:
Jędrzej Stuczyński
2024-09-24 16:44:05 +01:00
parent 8c979e3bac
commit 304b192f52
9 changed files with 572 additions and 356 deletions
+73 -4
View File
@@ -29,6 +29,7 @@ use std::fmt::{Debug, Display, Formatter};
use std::net::IpAddr;
use std::ops::{Deref, DerefMut};
use std::{fmt, time::Duration};
use thiserror::Error;
use time::{Date, OffsetDateTime};
use utoipa::{IntoParams, ToResponse, ToSchema};
@@ -192,6 +193,15 @@ pub struct MixNodeBondAnnotated {
pub ip_addresses: Vec<IpAddr>,
}
#[derive(Debug, Error)]
pub enum MalformedNodeBond {
#[error("the associated ed25519 identity key is malformed")]
InvalidEd25519Key,
#[error("the associated x25519 sphinx key is malformed")]
InvalidX25519Key,
}
impl MixNodeBondAnnotated {
pub fn mix_node(&self) -> &MixNode {
&self.mixnode_details.bond_information.mix_node
@@ -212,6 +222,32 @@ impl MixNodeBondAnnotated {
pub fn version(&self) -> &str {
&self.mixnode_details.bond_information.mix_node.version
}
pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
Ok(SkimmedNode {
node_id: self.mix_id(),
ed25519_identity_pubkey: self
.identity_key()
.parse()
.map_err(|_| MalformedNodeBond::InvalidEd25519Key)?,
ip_addresses: self.ip_addresses.clone(),
mix_port: self.mix_node().mix_port,
x25519_sphinx_pubkey: self
.mix_node()
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
epoch_role: role,
supported_roles: DeclaredRoles {
mixnode: true,
entry: false,
exit_nr: false,
exit_ipr: false,
},
entry: None,
performance: self.node_performance.last_24h,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, ToSchema)]
@@ -242,6 +278,39 @@ impl GatewayBondAnnotated {
pub fn owner(&self) -> &Addr {
self.gateway_bond.bond.owner()
}
pub fn try_to_skimmed_node(&self, role: NodeRole) -> Result<SkimmedNode, MalformedNodeBond> {
Ok(SkimmedNode {
node_id: self.gateway_bond.node_id,
ip_addresses: self.ip_addresses.clone(),
ed25519_identity_pubkey: self
.gateway_bond
.gateway
.identity_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidEd25519Key)?,
mix_port: self.gateway_bond.bond.gateway.mix_port,
x25519_sphinx_pubkey: self
.gateway_bond
.gateway
.sphinx_key
.parse()
.map_err(|_| MalformedNodeBond::InvalidX25519Key)?,
epoch_role: role,
supported_roles: DeclaredRoles {
mixnode: false,
entry: true,
exit_nr: false,
exit_ipr: false,
},
entry: Some(BasicEntryInformation {
hostname: None,
ws_port: self.gateway_bond.bond.gateway.clients_port,
wss_port: None,
}),
performance: self.node_performance.last_24h,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
@@ -640,7 +709,7 @@ impl NymNodeDescription {
}
}
pub fn try_to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode {
pub fn to_skimmed_node(&self, role: NodeRole, performance: Performance) -> SkimmedNode {
let keys = &self.description.host_information.keys;
let entry = if self.description.declared_role.entry {
Some(self.entry_information())
@@ -650,15 +719,15 @@ impl NymNodeDescription {
SkimmedNode {
node_id: self.node_id,
ed25519_identity_pubkey: keys.ed25519.clone(),
ed25519_identity_pubkey: keys.ed25519,
ip_addresses: self.description.host_information.ip_address.clone(),
mix_port: self.description.mix_port(),
x25519_sphinx_pubkey: keys.x25519.clone(),
x25519_sphinx_pubkey: keys.x25519,
// we can't use the declared roles, we have to take whatever was provided in the contract.
// why? say this node COULD operate as an exit, but it might be the case the contract decided
// to assign it an ENTRY role only. we have to use that one instead.
epoch_role: role,
supported_roles: self.description.declared_role.into(),
supported_roles: self.description.declared_role,
entry,
performance,
}
+21 -83
View File
@@ -1,11 +1,8 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::models::{
DeclaredRoles, GatewayBondAnnotated, MixNodeBondAnnotated, NymNodeData,
OffsetDateTimeJsonSchemaWrapper,
};
use crate::pagination::PaginatedResponse;
use crate::models::{DeclaredRoles, NymNodeData, OffsetDateTimeJsonSchemaWrapper};
use crate::pagination::{PaginatedResponse, Pagination};
use nym_crypto::asymmetric::ed25519::serde_helpers::bs58_ed25519_pubkey;
use nym_crypto::asymmetric::x25519::serde_helpers::bs58_x25519_pubkey;
use nym_crypto::asymmetric::{ed25519, x25519};
@@ -43,6 +40,25 @@ pub struct PaginatedCachedNodesResponse<T> {
pub nodes: PaginatedResponse<T>,
}
impl<T> PaginatedCachedNodesResponse<T> {
pub fn new_full(
refreshed_at: impl Into<OffsetDateTimeJsonSchemaWrapper>,
nodes: Vec<T>,
) -> Self {
PaginatedCachedNodesResponse {
refreshed_at: refreshed_at.into(),
nodes: PaginatedResponse {
pagination: Pagination {
total: nodes.len(),
page: 0,
size: nodes.len(),
},
data: nodes,
},
}
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, schemars::JsonSchema, utoipa::ToSchema)]
#[serde(rename_all = "kebab-case")]
pub enum NodeRoleQueryParam {
@@ -125,86 +141,8 @@ impl SkimmedNode {
_ => None,
}
}
// pub fn from_described_gateway(
// annotated: &GatewayBondAnnotated,
// description: Option<&NymNodeData>,
// ) -> Self {
// let mut base: SkimmedNode = annotated.into();
// let Some(description) = description else {
// return base;
// };
//
// // safety: the conversion always sets the entry field
// let entry = base.entry.as_mut().unwrap();
// entry
// .hostname
// .clone_from(&description.host_information.hostname);
// entry.ws_port = description.mixnet_websockets.ws_port;
// entry.wss_port = description.mixnet_websockets.wss_port;
//
// // always prefer self-described data
// if !description.host_information.ip_address.is_empty() {
// base.ip_addresses
// .clone_from(&description.host_information.ip_address)
// }
//
// base.supported_roles = description.declared_role;
//
// base
// }
}
// impl<'a> From<&'a MixNodeBondAnnotated> for SkimmedNode {
// fn from(value: &'a MixNodeBondAnnotated) -> Self {
// todo!()
// // SkimmedNode {
// // node_id: value.mix_id(),
// // ed25519_identity_pubkey: value.identity_key().to_string(),
// // ip_addresses: value.ip_addresses.clone(),
// // mix_port: value.mix_node().mix_port,
// // x25519_sphinx_pubkey: value.mix_node().sphinx_key.clone(),
// // epoch_role: NodeRole::Mixnode {
// // layer: value.mixnode_details.bond_information.layer.into(),
// // },
// // supported_roles: DeclaredRoles {
// // mixnode: true,
// // entry: false,
// // exit_nr: false,
// // exit_ipr: false,
// // },
// // entry: None,
// // performance: value.node_performance.last_24h,
// // }
// }
// }
// impl<'a> From<&'a GatewayBondAnnotated> for SkimmedNode {
// fn from(value: &'a GatewayBondAnnotated) -> Self {
// todo!()
// // SkimmedNode {
// // node_id: value.gateway_bond.node_id,
// // ip_addresses: value.ip_addresses.clone(),
// // ed25519_identity_pubkey: value.gateway_bond.bond.identity().clone(),
// // mix_port: value.gateway_bond.bond.gateway.mix_port,
// // x25519_sphinx_pubkey: value.gateway_bond.bond.gateway.sphinx_key.clone(),
// // epoch_role: NodeRole::EntryGateway,
// // supported_roles: DeclaredRoles {
// // mixnode: false,
// // entry: true,
// // exit_nr: false,
// // exit_ipr: false,
// // },
// // entry: Some(BasicEntryInformation {
// // hostname: None,
// // ws_port: value.gateway_bond.bond.gateway.clients_port,
// // wss_port: None,
// // }),
// // performance: value.node_performance.last_24h,
// // }
// }
// }
// an intermediate variant that exposes additional data such as noise keys but without
// the full fat of the self-described data
#[derive(Clone, Debug, Serialize, Deserialize, schemars::JsonSchema, ToSchema)]
+3
View File
@@ -1,6 +1,9 @@
// Copyright 2020-2023 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
#![warn(clippy::todo)]
#![warn(clippy::dbg_macro)]
use crate::epoch_operations::EpochAdvancer;
use crate::support::cli;
use crate::support::storage;
@@ -9,7 +9,7 @@ use crate::support::caching::cache::SharedCache;
use nym_api_requests::legacy::{LegacyGatewayBondWithId, LegacyMixNodeBondWithLayer};
use nym_api_requests::models::NymNodeDescription;
use nym_crypto::asymmetric::{encryption, identity};
use nym_mixnet_contract_common::{LegacyMixLayer, NodeId, RewardedSet};
use nym_mixnet_contract_common::{LegacyMixLayer, NodeId};
use nym_node_tester_utils::node::TestableNode;
use nym_node_tester_utils::NodeTester;
use nym_sphinx::acknowledgements::AckKey;
@@ -344,7 +344,7 @@ impl PacketPreparer {
let mixing_nym_nodes = descriptions.mixing_nym_nodes();
// last I checked `gatewaying` wasn't a word : )
let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes();
let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes();
let mut rng = thread_rng();
@@ -527,7 +527,7 @@ impl PacketPreparer {
.await
.expect("the cache must have been initialised!");
let mixing_nym_nodes = descriptions.mixing_nym_nodes();
let gateway_capable_nym_nodes = descriptions.gateway_capable_nym_nodes();
let gateway_capable_nym_nodes = descriptions.entry_capable_nym_nodes();
let (mixnodes, invalid_mixnodes) = self.filter_outdated_and_malformed_mixnodes(mixnodes);
let (gateways, invalid_gateways) = self.filter_outdated_and_malformed_gateways(gateways);
+10 -6
View File
@@ -11,7 +11,6 @@ use async_trait::async_trait;
use futures::{stream, StreamExt};
use nym_api_requests::models::{DescribedNodeType, NymNodeData, NymNodeDescription};
use nym_config::defaults::DEFAULT_NYM_NODE_HTTP_PORT;
use nym_crypto::asymmetric::{ed25519, encryption, identity, x25519};
use nym_mixnet_contract_common::{LegacyMixLayer, NodeId};
use nym_node_requests::api::client::{NymNodeApiClientError, NymNodeApiClientExt};
use nym_topology::gateway::GatewayConversionError;
@@ -159,25 +158,30 @@ impl DescribedNodes {
self.nodes.get(node_id)
}
pub fn all_nodes<'a>(&'a self) -> impl Iterator<Item = &'a NymNodeDescription> + 'a {
pub fn all_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
self.nodes.values()
}
pub fn mixing_nym_nodes<'a>(&'a self) -> impl Iterator<Item = &'a NymNodeDescription> + 'a {
pub fn mixing_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
.filter(|n| n.description.declared_role.mixnode)
}
pub fn gateway_capable_nym_nodes<'a>(
&'a self,
) -> impl Iterator<Item = &'a NymNodeDescription> + 'a {
pub fn entry_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
.filter(|n| n.description.declared_role.entry)
}
pub fn exit_capable_nym_nodes(&self) -> impl Iterator<Item = &NymNodeDescription> {
self.nodes
.values()
.filter(|n| n.contract_node_type == DescribedNodeType::NymNode)
.filter(|n| n.description.declared_role.can_operate_exit_gateway())
}
}
pub struct NodeDescriptionProvider {
@@ -0,0 +1,21 @@
// Copyright 2024 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: GPL-3.0-only
use nym_api_requests::models::OffsetDateTimeJsonSchemaWrapper;
use nym_bin_common::version_checker;
use time::OffsetDateTime;
pub(crate) fn refreshed_at(
iter: impl IntoIterator<Item = OffsetDateTime>,
) -> OffsetDateTimeJsonSchemaWrapper {
iter.into_iter().min().unwrap().into()
}
pub(crate) fn semver(requirement: &Option<String>, declared: &str) -> bool {
if let Some(semver_compat) = requirement.as_ref() {
if !version_checker::is_minor_version_compatible(declared, semver_compat) {
return false;
}
}
true
}
@@ -36,6 +36,7 @@ use nym_api_requests::nym_nodes::NodeRoleQueryParam;
use serde::Deserialize;
pub(crate) mod full_fat;
mod helpers;
pub(crate) mod semi_skimmed;
pub(crate) mod skimmed;
+386 -259
View File
@@ -2,16 +2,15 @@
// SPDX-License-Identifier: GPL-3.0-only
use crate::node_status_api::models::{AxumErrorResponse, AxumResult};
use crate::nym_nodes::handlers::unstable::helpers::{refreshed_at, semver};
use crate::nym_nodes::handlers::unstable::{NodesParams, NodesParamsWithRole};
use crate::support::http::helpers::PaginationRequest;
use crate::support::http::state::AppState;
use axum::extract::{Query, State};
use axum::Json;
use nym_api_requests::nym_nodes::{
CachedNodesResponse, NodeRole, NodeRoleQueryParam, PaginatedCachedNodesResponse, SkimmedNode,
};
use nym_bin_common::version_checker;
use std::collections::HashSet;
use tracing::trace;
pub type PaginatedSkimmedNodes = AxumResult<Json<PaginatedCachedNodesResponse<SkimmedNode>>>;
@@ -31,116 +30,15 @@ pub(super) async fn deprecated_gateways_basic(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
// TODO:
// 1. call '/v1/unstable/entry-gateways/mixnodes/skimmed/all'
// 2. remove pagination
// 1. call '/v1/unstable/skimmed/entry-gateways/all'
let all_gateways = entry_gateways_basic_all(state, query_params).await?;
// 3. return result
todo!()
// let status_cache = state.node_status_cache();
// let contract_cache = state.nym_contract_cache();
// let describe_cache = state.described_nodes_cache();
//
// // 1. get the rewarded set
// let rewarded_set = contract_cache
// .rewarded_set()
// .await
// .ok_or_else(AxumErrorResponse::internal)?;
//
// // determine which gateways are active, i.e. which gateways the clients should be using for connecting and routing the traffic
// let active_gateways = rewarded_set.gateways().into_iter().collect::<HashSet<_>>();
//
// // 2. grab all annotations so that we could attach scores to the [nym] nodes
// let annotations = status_cache
// .node_annotations()
// .await
// .ok_or_else(AxumErrorResponse::internal)?;
//
// // 3. grab all legacy gateways
// // due to legacy endpoints we already have fully annotated data on them
// let annotated_legacy_gateways = status_cache
// .annotated_legacy_gateways()
// .await
// .ok_or_else(AxumErrorResponse::internal)?;
//
// // 4. grab all relevant described nym-nodes
// let describe_cache = describe_cache.get().await?;
// let gateway_capable_nym_nodes = describe_cache.gateway_capable_nym_nodes();
//
// // 5. only return nodes that are present in the active set
// let mut active_skimmed_gateways = Vec::new();
//
// for (node_id, legacy) in annotated_legacy_gateways.iter() {
// if !active_gateways.contains(node_id) {
// continue;
// }
//
// if let Some(semver_compat) = semver_compatibility.as_ref() {
// let version = legacy.version();
// if !version_checker::is_minor_version_compatible(version, semver_compat) {
// continue;
// }
// }
//
// // if we have self-described info, prefer it over contract data
// if let Some(described) = describe_cache.get_node(node_id) {
// active_skimmed_gateways.push(
// described.to_skimmed_node(NodeRole::EntryGateway, legacy.node_performance.last_24h),
// )
// } else {
// active_skimmed_gateways.push(legacy.into());
// }
// }
//
// for nym_node in gateway_capable_nym_nodes {
// // if this node is not an active gateway, ignore it
// if !active_gateways.contains(&nym_node.node_id) {
// continue;
// }
//
// // if we have wrong version, ignore
// if let Some(semver_compat) = semver_compatibility.as_ref() {
// let version = nym_node.version();
// if !version_checker::is_minor_version_compatible(version, semver_compat) {
// continue;
// }
// }
//
// // NOTE: if we determined our node IS an active gateway, it MUST be EITHER entry or exit
// let role = if rewarded_set.is_exit(&nym_node.node_id) {
// NodeRole::ExitGateway
// } else {
// NodeRole::EntryGateway
// };
//
// // honestly, not sure under what exact circumstances this value could be missing,
// // but in that case just use 0 performance
// let annotation = annotations
// .get(&nym_node.node_id)
// .copied()
// .unwrap_or_default();
//
// active_skimmed_gateways
// .push(nym_node.to_skimmed_node(role, annotation.last_24h_performance));
// }
//
// // min of all caches
// let refreshed_at = [
// rewarded_set.timestamp(),
// annotations.timestamp(),
// describe_cache.timestamp(),
// annotated_legacy_gateways.timestamp(),
// ]
// .into_iter()
// .min()
// .unwrap()
// .into();
//
// Ok(Json(CachedNodesResponse {
// refreshed_at,
// nodes: active_skimmed_gateways,
// }))
Ok(Json(CachedNodesResponse {
refreshed_at: all_gateways.refreshed_at,
// 2. remove pagination
nodes: all_gateways.0.nodes.data,
}))
}
/// Deprecated query that gets ACTIVE-ONLY mixnodes
@@ -159,123 +57,15 @@ pub(super) async fn deprecated_mixnodes_basic(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> AxumResult<Json<CachedNodesResponse<SkimmedNode>>> {
// TODO:
// 1. call '/v1/unstable/nym-nodes/mixnodes/skimmed/active'
// 2. remove pagination
// 3. return result
// 1. call '/v1/unstable/nym-nodes/skimmed/mixnodes/active'
let active_mixnodes = mixnodes_basic_active(state, query_params).await?;
todo!()
//
// let status_cache = state.node_status_cache();
// let contract_cache = state.nym_contract_cache();
// let describe_cache = state.described_nodes_cache();
//
// // 1. get the rewarded set
// let rewarded_set = contract_cache
// .rewarded_set()
// .await
// .ok_or_else(AxumErrorResponse::internal)?;
//
// // determine which mixnodes are active, i.e. which mixnodes the clients should be using for routing the traffic
// let active_mixnodes = rewarded_set
// .active_mixnodes()
// .into_iter()
// .collect::<HashSet<_>>();
//
// // 2. grab all annotations so that we could attach scores to the [nym] nodes
// let annotations = status_cache
// .node_annotations()
// .await
// .ok_or_else(AxumErrorResponse::internal)?;
//
// // 3. grab all legacy mixnodes
// // due to legacy endpoints we already have fully annotated data on them
// let annotated_legacy_mixnodes = status_cache
// .annotated_legacy_mixnodes()
// .await
// .ok_or_else(AxumErrorResponse::internal)?;
//
// // 4. grab all relevant described nym-nodes
// let describe_cache = describe_cache.get().await?;
// let mixing_nym_nodes = describe_cache.mixing_nym_nodes();
//
// // TODO: in the future, only use the self-described cache and simply reject mixnodes that did not expose it
//
// // 5. only return nodes that are present in the active set
// let mut active_skimmed_mixnodes = Vec::new();
//
// for (mix_id, legacy) in annotated_legacy_mixnodes.iter() {
// if !active_mixnodes.contains(mix_id) {
// continue;
// }
//
// if let Some(semver_compat) = semver_compatibility.as_ref() {
// let version = legacy.version();
// if !version_checker::is_minor_version_compatible(version, semver_compat) {
// continue;
// }
// }
//
// // if we have self-described info, prefer it over contract data
// if let Some(described) = describe_cache.get_node(mix_id) {
// active_skimmed_mixnodes.push(described.to_skimmed_node(
// NodeRole::Mixnode {
// layer: legacy.mixnode_details.bond_information.layer.into(),
// },
// legacy.node_performance.last_24h,
// ))
// } else {
// active_skimmed_mixnodes.push(legacy.into());
// }
// }
//
// for nym_node in mixing_nym_nodes {
// // if this node is not an active mixnode, ignore it
// if !active_mixnodes.contains(&nym_node.node_id) {
// continue;
// }
//
// // if we have wrong version, ignore
// if let Some(semver_compat) = semver_compatibility.as_ref() {
// let version = nym_node.version();
// if !version_checker::is_minor_version_compatible(version, semver_compat) {
// continue;
// }
// }
//
// // SAFETY: if we determined our node IS active, it MUST have a layer
// // no other thread could have updated the rewarded set as we're still holding the [read] lock on the data
// #[allow(clippy::unwrap_used)]
// let layer = rewarded_set.try_get_mix_layer(&nym_node.node_id).unwrap();
//
// // honestly, not sure under what exact circumstances this value could be missing,
// // but in that case just use 0 performance
// let annotation = annotations
// .get(&nym_node.node_id)
// .copied()
// .unwrap_or_default();
//
// active_skimmed_mixnodes.push(
// nym_node.to_skimmed_node(NodeRole::Mixnode { layer }, annotation.last_24h_performance),
// );
// }
//
// // min of all caches
// let refreshed_at = [
// rewarded_set.timestamp(),
// annotations.timestamp(),
// describe_cache.timestamp(),
// annotated_legacy_mixnodes.timestamp(),
// ]
// .into_iter()
// .min()
// .unwrap()
// .into();
//
// Ok(Json(CachedNodesResponse {
// refreshed_at,
// nodes: active_skimmed_mixnodes,
// }))
// 3. return result
Ok(Json(CachedNodesResponse {
refreshed_at: active_mixnodes.refreshed_at,
// 2. remove pagination
nodes: active_mixnodes.0.nodes.data,
}))
}
/// Return all Nym Nodes and optionally legacy mixnodes/gateways (if `no-legacy` flag is not used)
@@ -292,24 +82,22 @@ pub(super) async fn deprecated_mixnodes_basic(
)]
pub(super) async fn nodes_basic(
state: State<AppState>,
query_params: Query<NodesParamsWithRole>,
Query(query_params): Query<NodesParamsWithRole>,
) -> PaginatedSkimmedNodes {
if let Some(role) = query_params.0.role {
if let Some(role) = query_params.role {
return match role {
NodeRoleQueryParam::ActiveMixnode => {
mixnodes_basic_all(state, Query(query_params.0.into())).await
mixnodes_basic_all(state, Query(query_params.into())).await
}
NodeRoleQueryParam::EntryGateway => {
entry_gateways_basic_all(state, Query(query_params.0.into())).await
entry_gateways_basic_all(state, Query(query_params.into())).await
}
NodeRoleQueryParam::ExitGateway => {
exit_gateways_basic_all(state, Query(query_params.0.into())).await
exit_gateways_basic_all(state, Query(query_params.into())).await
}
};
}
let query_params = query_params.0;
// TODO: implement pagination
let semver = query_params.semver_compatibility;
@@ -343,6 +131,9 @@ pub(super) async fn nodes_basic_active(
state: State<AppState>,
query_params: Query<NodesParamsWithRole>,
) -> PaginatedSkimmedNodes {
// TODO: implement it
let _ = query_params.per_page;
let _ = query_params.page;
todo!()
}
@@ -362,9 +153,123 @@ pub(super) async fn nodes_basic_standby(
state: State<AppState>,
query_params: Query<NodesParamsWithRole>,
) -> PaginatedSkimmedNodes {
// TODO: implement it
let _ = query_params.per_page;
let _ = query_params.page;
todo!()
}
async fn mixnodes_basic(
state: State<AppState>,
Query(query_params): Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
// TODO: implement it
let _ = query_params.per_page;
let _ = query_params.page;
let semver_compatibility = query_params.semver_compatibility;
// 1. get the rewarded set
let rewarded_set = state.rewarded_set().await?;
// 2. grab all annotations so that we could attach scores to the [nym] nodes
let annotations = state.node_annotations().await?;
// 3. grab all relevant described nym-nodes
let describe_cache = state.describe_nodes_cache_data().await?;
let mixing_nym_nodes = describe_cache.mixing_nym_nodes();
// 4. start building the response
let mut nodes = Vec::new();
for nym_node in mixing_nym_nodes {
let node_id = nym_node.node_id;
// if this node is not an active mixnode, ignore it
if active_only && !rewarded_set.is_active_mixnode(&node_id) {
continue;
}
// if we have wrong version, ignore
if !semver(&semver_compatibility, nym_node.version()) {
continue;
}
let role = match rewarded_set.try_get_mix_layer(&node_id) {
Some(layer) => NodeRole::Mixnode { layer },
None => NodeRole::Inactive,
};
// honestly, not sure under what exact circumstances this value could be missing,
// but in that case just use 0 performance
let annotation = annotations.get(&node_id).copied().unwrap_or_default();
nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance));
}
// 5. if we allow legacy mixnodes, repeat the procedure for mixnodes, otherwise return just nym-nodes
if query_params.no_legacy {
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
]);
return Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)));
}
// 6. grab all legacy mixnodes
// due to legacy endpoints we already have fully annotated data on them
let annotated_legacy_mixnodes = state.legacy_mixnode_annotations().await?;
for (mix_id, legacy) in annotated_legacy_mixnodes.iter() {
// if this node is not an active mixnode, ignore it
if active_only && !rewarded_set.is_active_mixnode(mix_id) {
continue;
}
// if we have wrong version, ignore
if !semver(&semver_compatibility, legacy.version()) {
continue;
}
let role = match rewarded_set.try_get_mix_layer(mix_id) {
Some(layer) => NodeRole::Mixnode { layer },
None => NodeRole::Inactive,
};
// if we have self-described info, prefer it over contract data
if let Some(described) = describe_cache.get_node(mix_id) {
nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h))
} else {
match legacy.try_to_skimmed_node(role) {
Ok(node) => nodes.push(node),
Err(err) => {
let id = legacy.identity_key();
trace!("node {id} is malformed: {err}")
}
}
}
}
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
annotated_legacy_mixnodes.timestamp(),
]);
Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)))
}
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
/// that are currently bonded and support mixing role.
#[utoipa::path(
@@ -381,7 +286,7 @@ pub(super) async fn mixnodes_basic_all(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> PaginatedSkimmedNodes {
todo!()
mixnodes_basic(state, query_params, false).await
}
/// Returns Nym Nodes and optionally legacy mixnodes (if `no-legacy` flag is not used)
@@ -400,7 +305,118 @@ pub(super) async fn mixnodes_basic_active(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> PaginatedSkimmedNodes {
todo!()
mixnodes_basic(state, query_params, true).await
}
async fn entry_gateways_basic(
state: State<AppState>,
Query(query_params): Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
// TODO: implement it
let _ = query_params.per_page;
let _ = query_params.page;
let semver_compatibility = query_params.semver_compatibility;
// 1. get the rewarded set
let rewarded_set = state.rewarded_set().await?;
// 2. grab all annotations so that we could attach scores to the [nym] nodes
let annotations = state.node_annotations().await?;
// 3. grab all relevant described nym-nodes
let describe_cache = state.describe_nodes_cache_data().await?;
let gateway_capable_nym_nodes = describe_cache.entry_capable_nym_nodes();
// 4. start building the response
let mut nodes = Vec::new();
for nym_node in gateway_capable_nym_nodes {
let node_id = nym_node.node_id;
// if this node is not an active gateway, ignore it
if active_only && !rewarded_set.is_entry(&node_id) {
continue;
}
// if we have wrong version, ignore
if !semver(&semver_compatibility, nym_node.version()) {
continue;
}
let role = match rewarded_set.is_entry(&node_id) {
true => NodeRole::EntryGateway,
false => NodeRole::Inactive,
};
// honestly, not sure under what exact circumstances this value could be missing,
// but in that case just use 0 performance
let annotation = annotations.get(&node_id).copied().unwrap_or_default();
nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance));
}
// 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes
if query_params.no_legacy {
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
]);
return Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)));
}
// 6. grab all legacy gateways
// due to legacy endpoints we already have fully annotated data on them
let annotated_legacy_gateways = state.legacy_gateways_annotations().await?;
for (node_id, legacy) in annotated_legacy_gateways.iter() {
// if this node is not an active gateway, ignore it
if active_only && !rewarded_set.is_entry(&node_id) {
continue;
}
// if we have wrong version, ignore
if !semver(&semver_compatibility, legacy.version()) {
continue;
}
let role = match rewarded_set.is_entry(&node_id) {
true => NodeRole::EntryGateway,
false => NodeRole::Inactive,
};
// if we have self-described info, prefer it over contract data
if let Some(described) = describe_cache.get_node(node_id) {
nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h))
} else {
match legacy.try_to_skimmed_node(role) {
Ok(node) => nodes.push(node),
Err(err) => {
let id = legacy.gateway_bond.identity();
trace!("node {id} is malformed: {err}")
}
}
}
}
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
annotated_legacy_gateways.timestamp(),
]);
Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)))
}
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
@@ -419,26 +435,7 @@ pub(super) async fn entry_gateways_basic_active(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> PaginatedSkimmedNodes {
todo!()
}
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
/// that are currently bonded and are in the active set with the exit role.
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(NodesParamsWithRole),
path = "/exit-gateways/active",
context_path = "/v1/unstable/nym-nodes/skimmed",
responses(
(status = 200, body = PaginatedCachedNodesResponse<SkimmedNode>)
)
)]
pub(super) async fn exit_gateways_basic_active(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> PaginatedSkimmedNodes {
todo!()
entry_gateways_basic(state, query_params, true).await
}
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
@@ -457,7 +454,137 @@ pub(super) async fn entry_gateways_basic_all(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> PaginatedSkimmedNodes {
todo!()
entry_gateways_basic(state, query_params, false).await
}
async fn exit_gateways_basic(
state: State<AppState>,
Query(query_params): Query<NodesParams>,
active_only: bool,
) -> PaginatedSkimmedNodes {
// TODO: implement it
let _ = query_params.per_page;
let _ = query_params.page;
let semver_compatibility = query_params.semver_compatibility;
// 1. get the rewarded set
let rewarded_set = state.rewarded_set().await?;
// 2. grab all annotations so that we could attach scores to the [nym] nodes
let annotations = state.node_annotations().await?;
// 3. grab all relevant described nym-nodes
let describe_cache = state.describe_nodes_cache_data().await?;
let gateway_capable_nym_nodes = describe_cache.exit_capable_nym_nodes();
// 4. start building the response
let mut nodes = Vec::new();
for nym_node in gateway_capable_nym_nodes {
let node_id = nym_node.node_id;
// if this node is not an active gateway, ignore it
if active_only && !rewarded_set.is_exit(&node_id) {
continue;
}
// if we have wrong version, ignore
if !semver(&semver_compatibility, nym_node.version()) {
continue;
}
let role = match rewarded_set.is_exit(&node_id) {
true => NodeRole::ExitGateway,
false => NodeRole::Inactive,
};
// honestly, not sure under what exact circumstances this value could be missing,
// but in that case just use 0 performance
let annotation = annotations.get(&node_id).copied().unwrap_or_default();
nodes.push(nym_node.to_skimmed_node(role, annotation.last_24h_performance));
}
// 5. if we allow legacy gateways, repeat the procedure for gateways, otherwise return just nym-nodes
if query_params.no_legacy {
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
]);
return Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)));
}
// 6. grab all legacy gateways
// due to legacy endpoints we already have fully annotated data on them
let annotated_legacy_gateways = state.legacy_gateways_annotations().await?;
for (node_id, legacy) in annotated_legacy_gateways.iter() {
// if this node is not an active gateway, ignore it
if active_only && !rewarded_set.is_exit(&node_id) {
continue;
}
// if we have wrong version, ignore
if !semver(&semver_compatibility, legacy.version()) {
continue;
}
let role = match rewarded_set.is_exit(&node_id) {
true => NodeRole::ExitGateway,
false => NodeRole::Inactive,
};
// if we have self-described info, prefer it over contract data
if let Some(described) = describe_cache.get_node(node_id) {
nodes.push(described.to_skimmed_node(role, legacy.node_performance.last_24h))
} else {
match legacy.try_to_skimmed_node(role) {
Ok(node) => nodes.push(node),
Err(err) => {
let id = legacy.gateway_bond.identity();
trace!("node {id} is malformed: {err}")
}
}
}
}
// min of all caches
let refreshed_at = refreshed_at([
rewarded_set.timestamp(),
annotations.timestamp(),
describe_cache.timestamp(),
annotated_legacy_gateways.timestamp(),
]);
Ok(Json(PaginatedCachedNodesResponse::new_full(
refreshed_at,
nodes,
)))
}
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
/// that are currently bonded and are in the active set with the exit role.
#[utoipa::path(
tag = "Unstable Nym Nodes",
get,
params(NodesParamsWithRole),
path = "/exit-gateways/active",
context_path = "/v1/unstable/nym-nodes/skimmed",
responses(
(status = 200, body = PaginatedCachedNodesResponse<SkimmedNode>)
)
)]
pub(super) async fn exit_gateways_basic_active(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> PaginatedSkimmedNodes {
exit_gateways_basic(state, query_params, true).await
}
/// Returns Nym Nodes and optionally legacy gateways (if `no-legacy` flag is not used)
@@ -476,5 +603,5 @@ pub(super) async fn exit_gateways_basic_all(
state: State<AppState>,
query_params: Query<NodesParams>,
) -> PaginatedSkimmedNodes {
todo!()
exit_gateways_basic(state, query_params, false).await
}
+54 -1
View File
@@ -5,11 +5,17 @@ use crate::circulating_supply_api::cache::CirculatingSupplyCache;
use crate::network::models::NetworkDetails;
use crate::node_describe_cache::DescribedNodes;
use crate::node_status_api::handlers::unstable;
use crate::node_status_api::models::AxumErrorResponse;
use crate::node_status_api::NodeStatusCache;
use crate::nym_contract_cache::cache::NymContractCache;
use crate::nym_contract_cache::cache::{CachedRewardedSet, NymContractCache};
use crate::support::caching::cache::SharedCache;
use crate::support::caching::Cache;
use crate::support::storage;
use nym_api_requests::models::{GatewayBondAnnotated, MixNodeBondAnnotated, NodeAnnotation};
use nym_mixnet_contract_common::NodeId;
use nym_task::TaskManager;
use std::collections::HashMap;
use tokio::sync::RwLockReadGuard;
use tokio::task::JoinHandle;
use tokio_util::sync::CancellationToken;
@@ -102,3 +108,50 @@ impl AppState {
&self.node_info_cache
}
}
// handler helpers to easily get data or return error response
impl AppState {
pub(crate) async fn describe_nodes_cache_data(
&self,
) -> Result<RwLockReadGuard<Cache<DescribedNodes>>, AxumErrorResponse> {
Ok(self.described_nodes_cache().get().await?)
}
pub(crate) async fn rewarded_set(
&self,
) -> Result<RwLockReadGuard<Cache<CachedRewardedSet>>, AxumErrorResponse> {
self.nym_contract_cache()
.rewarded_set()
.await
.ok_or_else(AxumErrorResponse::internal)
}
pub(crate) async fn node_annotations(
&self,
) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, NodeAnnotation>>>, AxumErrorResponse> {
self.node_status_cache()
.node_annotations()
.await
.ok_or_else(AxumErrorResponse::internal)
}
pub(crate) async fn legacy_mixnode_annotations(
&self,
) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, MixNodeBondAnnotated>>>, AxumErrorResponse>
{
self.node_status_cache()
.annotated_legacy_mixnodes()
.await
.ok_or_else(AxumErrorResponse::internal)
}
pub(crate) async fn legacy_gateways_annotations(
&self,
) -> Result<RwLockReadGuard<Cache<HashMap<NodeId, GatewayBondAnnotated>>>, AxumErrorResponse>
{
self.node_status_cache()
.annotated_legacy_gateways()
.await
.ok_or_else(AxumErrorResponse::internal)
}
}